Merge remote-tracking branch 'origin/main' into 34485

This commit is contained in:
Ying WANG 2021-05-25 14:24:44 +02:00
commit 51b11fb74c
196 changed files with 3797 additions and 2152 deletions

View File

@ -755,8 +755,8 @@ steps:
image: grafana/build-container:1.4.1 image: grafana/build-container:1.4.1
commands: commands:
- "[ $(grep FocusConvey -R pkg | wc -l) -eq \"0\" ] || exit 1" - "[ $(grep FocusConvey -R pkg | wc -l) -eq \"0\" ] || exit 1"
- ./bin/grabpl test-backend --edition oss - ./bin/grabpl test-backend --edition oss --tries 5
- ./bin/grabpl integration-tests --edition oss - ./bin/grabpl integration-tests --edition oss --tries 5
depends_on: depends_on:
- initialize - initialize
@ -1130,8 +1130,8 @@ steps:
image: grafana/build-container:1.4.1 image: grafana/build-container:1.4.1
commands: commands:
- "[ $(grep FocusConvey -R pkg | wc -l) -eq \"0\" ] || exit 1" - "[ $(grep FocusConvey -R pkg | wc -l) -eq \"0\" ] || exit 1"
- ./bin/grabpl test-backend --edition enterprise - ./bin/grabpl test-backend --edition enterprise --tries 5
- ./bin/grabpl integration-tests --edition enterprise - ./bin/grabpl integration-tests --edition enterprise --tries 5
depends_on: depends_on:
- initialize - initialize
@ -1196,8 +1196,8 @@ steps:
image: grafana/build-container:1.4.1 image: grafana/build-container:1.4.1
commands: commands:
- "[ $(grep FocusConvey -R pkg | wc -l) -eq \"0\" ] || exit 1" - "[ $(grep FocusConvey -R pkg | wc -l) -eq \"0\" ] || exit 1"
- ./bin/grabpl test-backend --edition enterprise2 - ./bin/grabpl test-backend --edition enterprise2 --tries 5
- ./bin/grabpl integration-tests --edition enterprise2 - ./bin/grabpl integration-tests --edition enterprise2 --tries 5
depends_on: depends_on:
- initialize - initialize
@ -1710,8 +1710,8 @@ steps:
image: grafana/build-container:1.4.1 image: grafana/build-container:1.4.1
commands: commands:
- "[ $(grep FocusConvey -R pkg | wc -l) -eq \"0\" ] || exit 1" - "[ $(grep FocusConvey -R pkg | wc -l) -eq \"0\" ] || exit 1"
- ./bin/grabpl test-backend --edition oss - ./bin/grabpl test-backend --edition oss --tries 5
- ./bin/grabpl integration-tests --edition oss - ./bin/grabpl integration-tests --edition oss --tries 5
depends_on: depends_on:
- initialize - initialize
@ -2074,8 +2074,8 @@ steps:
image: grafana/build-container:1.4.1 image: grafana/build-container:1.4.1
commands: commands:
- "[ $(grep FocusConvey -R pkg | wc -l) -eq \"0\" ] || exit 1" - "[ $(grep FocusConvey -R pkg | wc -l) -eq \"0\" ] || exit 1"
- ./bin/grabpl test-backend --edition enterprise - ./bin/grabpl test-backend --edition enterprise --tries 5
- ./bin/grabpl integration-tests --edition enterprise - ./bin/grabpl integration-tests --edition enterprise --tries 5
depends_on: depends_on:
- initialize - initialize
@ -2140,8 +2140,8 @@ steps:
image: grafana/build-container:1.4.1 image: grafana/build-container:1.4.1
commands: commands:
- "[ $(grep FocusConvey -R pkg | wc -l) -eq \"0\" ] || exit 1" - "[ $(grep FocusConvey -R pkg | wc -l) -eq \"0\" ] || exit 1"
- ./bin/grabpl test-backend --edition enterprise2 - ./bin/grabpl test-backend --edition enterprise2 --tries 5
- ./bin/grabpl integration-tests --edition enterprise2 - ./bin/grabpl integration-tests --edition enterprise2 --tries 5
depends_on: depends_on:
- initialize - initialize

View File

@ -139,10 +139,13 @@ connstr =
# This enables data proxy logging, default is false # This enables data proxy logging, default is false
logging = false logging = false
# How long the data proxy waits before timing out, default is 30 seconds. # How long the data proxy waits to read the headers of the response before timing out, default is 30 seconds.
# This setting also applies to core backend HTTP data sources where query requests use an HTTP client with timeout set. # This setting also applies to core backend HTTP data sources where query requests use an HTTP client with timeout set.
timeout = 30 timeout = 30
# How long the data proxy waits to establish a TCP connection before timing out, default is 10 seconds.
dialTimeout = 10
# How many seconds the data proxy waits before sending a keepalive request. # How many seconds the data proxy waits before sending a keepalive request.
keep_alive_seconds = 30 keep_alive_seconds = 30

View File

@ -145,10 +145,13 @@
# This enables data proxy logging, default is false # This enables data proxy logging, default is false
;logging = false ;logging = false
# How long the data proxy waits before timing out, default is 30 seconds. # How long the data proxy waits to read the headers of the response before timing out, default is 30 seconds.
# This setting also applies to core backend HTTP data sources where query requests use an HTTP client with timeout set. # This setting also applies to core backend HTTP data sources where query requests use an HTTP client with timeout set.
;timeout = 30 ;timeout = 30
# How long the data proxy waits to establish a TCP connection before timing out, default is 10 seconds.
;dialTimeout = 10
# How many seconds the data proxy waits before sending a keepalive probe request. # How many seconds the data proxy waits before sending a keepalive probe request.
;keep_alive_seconds = 30 ;keep_alive_seconds = 30

View File

@ -0,0 +1,2 @@
ORIGIN_SERVER=http://host.docker.internal:9090/
SLEEP_DURATION=60s

View File

@ -1,7 +1,10 @@
FROM golang:latest as builder
FROM golang:latest
ADD main.go / ADD main.go /
WORKDIR / WORKDIR /
RUN GO111MODULE=off go build -o main . RUN GO111MODULE=off CGO_ENABLED=0 go build -o main .
FROM scratch
WORKDIR /
EXPOSE 3011 EXPOSE 3011
COPY --from=builder /main /main
ENTRYPOINT ["/main"] ENTRYPOINT ["/main"]

View File

@ -3,4 +3,5 @@
ports: ports:
- '3011:3011' - '3011:3011'
environment: environment:
ORIGIN_SERVER: 'http://host.docker.internal:9090/' ORIGIN_SERVER: ${ORIGIN_SERVER}
SLEEP_DURATION: ${SLEEP_DURATION}

View File

@ -1,7 +1,6 @@
package main package main
import ( import (
"fmt"
"log" "log"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
@ -13,16 +12,26 @@ import (
func main() { func main() {
origin := os.Getenv("ORIGIN_SERVER") origin := os.Getenv("ORIGIN_SERVER")
if origin == "" { if origin == "" {
origin = "http://host.docker.internal:9090/" // it is never not-set, the default is in the `.env` file
log.Fatalf("missing env-variable ORIGIN_SERVER")
} }
sleep := time.Minute sleepDurationStr := os.Getenv("SLEEP_DURATION")
if sleepDurationStr == "" {
// it is never not-set, the default is in the `.env` file
log.Fatalf("missing env-variable SLEEP_DURATION")
}
sleep, err := time.ParseDuration(sleepDurationStr)
if err != nil {
log.Fatalf("failed to parse SLEEP_DURATION: %v", err)
}
originURL, _ := url.Parse(origin) originURL, _ := url.Parse(origin)
proxy := httputil.NewSingleHostReverseProxy(originURL) proxy := httputil.NewSingleHostReverseProxy(originURL)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Printf("sleeping for %s then proxying request: %s", sleep.String(), r.RequestURI) log.Printf("sleeping for %s then proxying request: url '%s', headers: '%v'", sleep.String(), r.RequestURI, r.Header)
<-time.After(sleep) <-time.After(sleep)
proxy.ServeHTTP(w, r) proxy.ServeHTTP(w, r)
}) })

View File

@ -575,3 +575,9 @@ The following sections detail the supported settings and secure settings for eac
| Name | | Name |
| ---- | | ---- |
| url | | url |
## Grafana Enterprise
Grafana Enterprise supports provisioning for the following resources:
- [Access Control Provisioning]({{< relref "../enterprise/access-control/provisioning.md" >}})

View File

@ -6,39 +6,34 @@ weight = 110
# Alerts overview # Alerts overview
Alerts allow you to identify problems in your system moments after they occur. By quickly identifying unintended changes in your system, you can minimize disruptions to your services. Alerts allow you to know about problems in your systems moments after they occur. Robust and actionable alerts help you identify and resolve issues quickly, minimizing disruption to your services.
Alerts consists of two parts: Alerts have four main components:
- Alert rules - When the alert is triggered. Alert rules are defined by one or more conditions that are regularly evaluated by Grafana. - Alert rule - One or more conditions, the frequency of evaluation, and the (optional) duration that a condition must be met before notifying.
- Notification channel - How the alert is delivered. When the conditions of an alert rule are met, the Grafana notifies the channels configured for that alert. - Contact point - A channel for sending notifications when the conditions of an alert rule are met.
- Notification policy - A set of matching and grouping criteria used to determine where, and how frequently, to send notifications.
Currently only the graph panel visualization supports alerts. - Silences - Date and matching criteria used to silence notifications.
## Alert tasks ## Alert tasks
You can perform the following tasks for alerts: You can perform the following tasks for alerts:
- [Add or edit an alert notification channel]({{< relref "notifications.md" >}})
- [Create an alert rule]({{< relref "create-alerts.md" >}}) - [Create an alert rule]({{< relref "create-alerts.md" >}})
- [View existing alert rules and their current state]({{< relref "view-alerts.md" >}}) - [View existing alert rules and their current state]({{< relref "view-alerts.md" >}})
- [Test alert rules and troubleshoot]({{< relref "troubleshoot-alerts.md" >}}) - [Test alert rules and troubleshoot]({{< relref "troubleshoot-alerts.md" >}})
- [Add or edit an alert contact point]({{< relref "notifications.md" >}})
## Clustering ## Clustering
Currently alerting supports a limited form of high availability. Since v4.2.0 of Grafana, alert notifications are deduped when running multiple servers. This means all alerts are executed on every server but no duplicate alert notifications are sent due to the deduping logic. Proper load balancing of alerts will be introduced in the future. Currently alerting supports a limited form of high availability. Since v4.2.0 of Grafana, alert notifications are deduped when running multiple servers. This means all alerts are executed on every server but no duplicate alert notifications are sent due to the deduping logic. Proper load balancing of alerts will be introduced in the future.
## Notifications ## Alert evaluation
You can also set alert rule notifications along with a detailed message about the alert rule. The message can contain anything: information about how you might solve the issue, link to runbook, and so on. Grafana managed alerts are evaluated by the Grafana backend. Rule evaluations are scheduled, according to the alert rule configuration, and queries are evaluated by an engine that is part of core Grafana.
The actual notifications are configured and shared between multiple alerts. Alert rules can only query backend data sources with alerting enabled:
- builtin or developed and maintained by grafana: `Graphite`, `Prometheus`, `Loki`, `InfluxDB`, `Elasticsearch`,
## Alert execution
Alert rules are evaluated in the Grafana backend in a scheduler and query execution engine that is part
of core Grafana. Alert rules can query only backend data sources with alerting enabled. Such data sources are:
- builtin or developed and maintained by grafana, such as: `Graphite`, `Prometheus`, `Loki`, `InfluxDB`, `Elasticsearch`,
`Google Cloud Monitoring`, `Cloudwatch`, `Azure Monitor`, `MySQL`, `PostgreSQL`, `MSSQL`, `OpenTSDB`, `Oracle`, and `Azure Data Explorer` `Google Cloud Monitoring`, `Cloudwatch`, `Azure Monitor`, `MySQL`, `PostgreSQL`, `MSSQL`, `OpenTSDB`, `Oracle`, and `Azure Data Explorer`
- any community backend data sources with alerting enabled (`backend` and `alerting` properties are set in the [plugin.json]({{< relref "../developers/plugins/metadata.md" >}})) - any community backend data sources with alerting enabled (`backend` and `alerting` properties are set in the [plugin.json]({{< relref "../developers/plugins/metadata.md" >}}))
@ -46,9 +41,12 @@ of core Grafana. Alert rules can query only backend data sources with alerting e
The alert engine publishes some internal metrics about itself. You can read more about how Grafana publishes [internal metrics]({{< relref "../administration/view-server/internal-metrics.md" >}}). The alert engine publishes some internal metrics about itself. You can read more about how Grafana publishes [internal metrics]({{< relref "../administration/view-server/internal-metrics.md" >}}).
Description | Type | Metric name Metric Name | Type | Description
---------- | ----------- | ---------- ---------- | ----------- | ----------
Total number of alerts | counter | `alerting.active_alerts` `alerting.alerts` | gauge | How many alerts by state
Alert execution result | counter | `alerting.result` `alerting.request_duration_seconds` | histogram | Histogram of requests to the Alerting API
Notifications sent counter | counter | `alerting.notifications_sent` `alerting.active_configurations` | gauge | The number of active, non default alertmanager configurations for grafana managed alerts
Alert execution timer | timer | `alerting.execution_time` `alerting.rule_evaluations_total` | counter | The total number of rule evaluations
`alerting.rule_evaluation_failures_total` | counter | The total number of rule evaluation failures
`alerting.rule_evaluation_duration_seconds` | summary | The duration for a rule to execute
`alerting.rule_group_rules` | gauge | The number of rules

View File

@ -1,5 +1,5 @@
+++ +++
title = "Time series" title = "Intro to time series"
description = "Introduction to time series" description = "Introduction to time series"
keywords = ["grafana", "intro", "guide", "concepts", "timeseries"] keywords = ["grafana", "intro", "guide", "concepts", "timeseries"]
weight = 400 weight = 400

View File

@ -43,6 +43,7 @@ With Grafana Enterprise [enhanced LDAP]({{< relref "enhanced_ldap.md" >}}), you
With Grafana Enterprise, you get access to new features, including: With Grafana Enterprise, you get access to new features, including:
- [Fine-grained access control]({{< relref "access-control/_index.md" >}}) to control access with fine-grained roles and permissions.
- [Data source permissions]({{< relref "datasource_permissions.md" >}}) to restrict query access to specific teams and users. - [Data source permissions]({{< relref "datasource_permissions.md" >}}) to restrict query access to specific teams and users.
- [Reporting]({{< relref "reporting.md" >}}) to generate a PDF report from any dashboard and set up a schedule to have it emailed to whoever you choose. - [Reporting]({{< relref "reporting.md" >}}) to generate a PDF report from any dashboard and set up a schedule to have it emailed to whoever you choose.
- [Export dashboard as PDF]({{< relref "export-pdf.md" >}}) - [Export dashboard as PDF]({{< relref "export-pdf.md" >}})

View File

@ -0,0 +1,60 @@
+++
title = "Fine-grained access control"
description = "Grant, change, or revoke access to Grafana resources"
keywords = ["grafana", "fine-grained-access-control", "roles", "permissions", "enterprise"]
weight = 100
+++
# Fine-grained access control
> **Note:** Fine-grained access control is in beta, and you can expect changes in future releases.
Fine-grained access control provides a standardized way of granting, changing, and revoking access when it comes to viewing and modifying Grafana resources, such as users and reports.
Fine-grained access control works alongside the current [Grafana permissions]({{< relref "../../permissions/_index.md" >}}), and it allows you granular control of users actions.
To learn more about how fine-grained access control works, refer to [Roles]({{< relref "./roles.md" >}}) and [Permissions]({{< relref "./permissions.md" >}}).
To use the fine-grained access control system, refer to [Fine-grained access control usage scenarios]({{< relref "./usage-scenarios.md" >}}).
## Access management
Fine-grained access control considers a) _who_ has an access (`identity`), and b) _what they can do_ and on which _Grafana resource_ (`role`).
You can grant, change, or revoke access to _users_ (`identity`). When an authenticated user tries to access a Grafana resource, the authorization system checks the required fine-grained permissions for the resource and determines whether or not the action is allowed. Refer to [Fine-grained permissions]({{< relref "./permissions.md" >}}) for a complete list of available permissions.
To grant or revoke access to your users, create or remove built-in role assignments. For more information, refer to [Built-in role assignments]({{< relref "./roles.md#built-in-role-assignments" >}}).
## Resources with fine-grained permissions
Fine-grained access control is currently available for [Reporting]({{< relref "../reporting.md" >}}) and [Managing Users]({{< relref "../../manage-users/_index.md" >}}).
To learn more about specific endpoints where you can use access control, refer to [Permissions]({{< relref "./permissions.md" >}}) and to the relevant API guide:
- [Fine-grained access control API]({{< relref "../../http_api/access_control.md" >}})
- [Admin API]({{< relref "../../http_api/admin.md" >}})
- [Organization API]({{< relref "../../http_api/org.md" >}})
- [Reporting API]({{< relref "../../http_api/reporting.md" >}})
- [User API]({{< relref "../../http_api/user.md" >}})
## Enable fine-grained access control
Fine-grained access control is available behind the `accesscontrol` feature toggle in Grafana Enterprise 8.0+.
You can enable it either in a [config file](({{< relref "../../administration/configuration.md#fconfig-file-locations" >}})) or by [configuring an environment variable](http://localhost:3002/docs/grafana/next/administration/configuration/#configure-with-environment-variables).
### Enable in config file
In your [config file](({{< relref "../../administration/configuration.md#config-file-locations" >}})), add `accesscontrol` as a [feature_toggle](({{< relref "../../administration/configuration.md#feature_toggle" >}})).
```
[feature_toggles]
# enable features, separated by spaces
enable = accesscontrol
```
### Enable with an environment variable
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.
### 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" >}}).

View File

@ -0,0 +1,75 @@
+++
title = "Permissions"
description = "Understand fine-grained access control permissions"
keywords = ["grafana", "fine-grained access-control", "roles", "permissions", "enterprise"]
weight = 115
+++
# Permissions
Each permission is defined by an action and a scope. When evaluating a fine-grained access decision, consider what specific action a user should be allowed to perform, and on what resources (its scope).
To grant permissions to a user, create built-in role assignments. A built-in role assignment is a *modification* to one of the existing built-in roles in Grafana (Viewer, Editor, Admin) For more information, refer to [Built-in role assignments]({{< relref "./roles.md#built-in-role-assignments" >}}).
To learn more about which permissions are used for which resources, refer to [Resources with fine-grained permissions]({{< relref "./_index.md#resources-with-fine-grained-permissions" >}}).
action
: The specific action on a resource defines what a user is allowed to perform if they have permission with the relevant action assigned to it.
scope
: The scope describes where an action can be performed, such as reading a specific user profile. In such case, a permission is associated with the scope `users:<userId>` to the relevant role. Also, you can combine multiple scopes by using the `/` delimiter.
## Action definitions
Note that below list is not exhaustive yet and more permissions will be available with further releases of fine-grained access control.
Action | Applicable scopes | Description
--- | --- | ---
roles:list | roles:* | Allows to list available roles without permissions.
roles:read | roles:* | Allows to read a specific role with it's permissions.
roles:write | permissions:delegate | Allows to create or update a custom role.
roles:delete | permissions:delegate | Allows to delete a custom role.
roles.builtin:list | roles:* | Allows to list built-in role assignments.
roles.builtin:add | permissions:delegate | Allows to create a built-in role assignment.
roles.builtin:remove | permissions:delegate | Allows to delete a built-in role assignment.
reports.admin:write | reports:* | Allows to create or update reports.
reports:delete | reports:* | Allows to delete reports.
reports:read | reports:* | Allows to list all available reports and to get a specific report.
reports:send | reports:* | Allows to send report email.
reports.settings:write | n/a | Allows to update report settings.
reports.settings:read | n/a | Allows to read report settings.
provisioning:reload | service:access-control | Allows to reload provisioning files after an update.
users:read | global:users:* | Allows to read, search user profiles.
users:write | global:users:* | Allows to update user profiles.
users.teams:read | global:users:* | Allows to read user teams.
users.authtoken:list | global:users:* | Allows to list auth tokens assigned to users.
users.authtoken:update | global:users:* | Allows to update auth tokens assigned to users.
users.password:update | global:users:* | Allows to update users password.
users:delete | global:users:* | Allows to delete users.
users:create | n/a | Allows to create users.
users:enable | global:users:* | Allows to enable users.
users:disable | global:users:* | Allows to disable users.
users.permissions:update | global:users:* | Allows to update users org level permissions.
users:logout | global:users:* | Allows to enforce logout for users.
users.quotas:list | global:users:* | Allows to list user quotas.
users.quotas:update | global:users:* | Allows to update user quotas.
org.users.read | users:* | Allows to get user profiles within the organization.
org.users.add | users:* | Allows to add users to the organization.
org.users.remove | users:* | Allows to remove users from the organization.
org.users.role:update | users:* | Allows to update users organization role for the assigned organization.
ldap.user:read | n/a | Allows to read LDAP users.
ldap.user:sync | n/a | Allows to sync LDAP users.
ldap.status:read | n/a | Allows to check LDAP status.
## Scope definitions
Note that below list is not exhaustive yet and more scopes will be available with further releases of fine-grained access control.
Scope | Description
--- | ---
roles:* | Indicates against what roles an action can be performed. For example, `roles:*` assumes any roles, and `roles:randomuid` assumes only a role with UID `randomuid`.
permissions:delegate | The scope is only applicable for roles associated with the Access Control itself and indicates that you can delegate your permissions only, or a subset of it, by creating a new role or making an assignment.
reports:* | Indicates against what reports an action can be performed.
service:access-control | Only relevant for provisioning and indicates that the action can be performed only for access control provisioning files.
global:users:* | Indicates that action can be performed against users globally.
users:* | Indicates that an action can be performed against users in organization level.

View File

@ -0,0 +1,140 @@
+++
title = "Provisioning roles and assignments"
description = "Understand how to provision roles and assignments in fine-grained access control"
keywords = ["grafana", "fine-grained-access-control", "roles", "provisioning", "assignments", "permissions", "enterprise"]
weight = 120
+++
# Provisioning
You can create, change or remove [Custom roles]({{< relref "./roles.md#custom-roles" >}}) and create or remove [built-in role assignments]({{< relref "./roles.md#built-in-role-assignments" >}}), by adding one or more YAML configuration files in the [`provisioning/access-control/`]({{< relref "../../administration/configuration/#provisioning" >}}) directory.
Refer to [Grafana provisioning]({{< relref "../../administration/configuration/#provisioning" >}}) to learn more about provisioning.
If you want to manage roles and built-in role assignments by API, refer to the [Fine-grained access control HTTP API]({{< relref "../../http_api/access_control/" >}}).
## Configuration
The configuration files must be located in the [`provisioning/access-control/`]({{< relref "../../administration/configuration/#provisioning" >}}) directory.
Grafana performs provisioning during the startup. Refer to the [Reload provisioning configurations]({{< relref "../../http_api/admin/#reload-provisioning-configurations" >}}) to understand how you can reload configuration at runtime.
## Manage custom roles
You can create, update and delete custom roles, as well as create and remove built-in role assignments for them.
### Create or update roles
To create or update custom roles, you can add a list of `roles` in the configuration.
Note that in order to update a role, you would need to increment the [version]({{< relref "./roles.md#custom-roles" >}}).
It is only possibly to provision [organization local]({{< relref "./roles#role-scopes" >}}) roles. For creating or updating _global_ roles, refer to the [Fine-grained access control HTTP API]({{< relref "../../http_api/access_control.md" >}}).
### Delete roles
To delete a role, you can add a list of roles under `deleteRoles` section in the configuration file. Note that deletion is performed after role insertion/update.
### Create and remove built-in role assignments
To create a built-in role assignment, you can add list of assignments under `builtInRoles` section in the configuration file, as an element of `roles`. To remove a built-in role assignment, leave `builtInRoles` list empty.
Note that it is only possibly to provision [organization local]({{< relref "./roles#built-in-role-assignments" >}}) assignments. For creating or updating _global_ assignments, refer to the [Fine-grained access control HTTP API]({{< relref "../../http_api/access_control.md" >}}).
## Manage default built-in role assignments
During the startup, Grafana creates [default built-in role assignments]({{< relref "./roles#default-built-in-role-assignments" >}}) with [predefined roles]({{< relref "./roles#predefined-roles" >}}). You can remove and add back later those assignments by using provisioning.
### Remove default assignment
To remove default built-in role assignment, you can use `removeDefaultAssignments` element in the configuration file. You would need to provide built-in role name and predefined role name.
### Add back default assignment
To add back default built-in role assignment, you can use `addDefaultAssignments` element in the configuration file. You would need to provide built-in role name and predefined role name.
## Example of a role configuration file
```yaml
# config file version
apiVersion: 1
# list of default built-in role assignments that should be removed
removeDefaultAssignments:
# <string>, must be one of the Organization roles (`Viewer`, `Editor`, `Admin`) or `Grafana Admin`
- builtInRole: "Grafana Admin"
# <string>, must be one of the existing predefined roles
predefinedRole: "grafana:roles:permissions:admin"
# list of default built-in role assignments that should be added back
addDefaultAssignments:
# <string>, must be one of the Organization roles (`Viewer`, `Editor`, `Admin`) or `Grafana Admin`
- builtInRole: "Admin"
# <string>, must be one of the existing predefined roles
predefinedRole: "grafana:roles:reporting:admin:read"
# list of roles that should be deleted
deleteRoles:
# <string> name of the role you want to create. Required if no uid is set
- name: ReportEditor
# <string> uid of the role. Required if no name
uid: reporteditor1
# <int> org id. will default to Grafana's default if not specified
orgId: 1
# <bool> force deletion revoking all grants of the role
force: true
# list of roles to insert/update depending on what is available in the database
roles:
# <string, required> name of the role you want to create. Required
- name: CustomEditor
# <string> uid of the role. Has to be unique for all orgs.
uid: customeditor1
# <string> description of the role, informative purpose only.
description: "Role for our custom user editors"
# <int> version of the role, Grafana will update the role when increased
version: 2
# <int> org id. will default to Grafana's default if not specified
orgId: 1
# <list> list of the permissions granted by this role
permissions:
# <string, required> action allowed
- action: "users:read"
#<string, required> scope it applies to
scope: "users:*"
- action: "users:write"
scope: "users:*"
- action: "users:create"
scope: "users:*"
# <list> list of builtIn roles the role should be assigned to
builtInRoles:
# <string, required> name of the builtin role you want to assign the role to
- name: "Editor"
# <int> org id. will default to the role org id
orgId: 1
```
## Supported settings
The following sections detail the supported settings for roles and built-in role assignments.
- Refer to [Permissions]({{< relref "./permissions.md#action-definitions" >}}) for full list of valid permissions.
- Check [Custom roles]({{< relref "./roles.md#custom-roles" >}}) to understand attributes for roles.
- The [default org ID]({{< relref "../../administration/configuration#auto_assign_org_id" >}}) is used if `orgId` is not specified in any of the configuration blocks.
## Validation rules
A basic set of validation rules are applied to the input `yaml` files.
### Roles
- `name` must not be empty
- `name` must not have `grafana:roles:` prefix.
### Built-in role assignments
- `name` must be one of the Organization roles (`Viewer`, `Editor`, `Admin`) or `Grafana Admin`.
- When `orgId` is not specified, it inherits the `orgId` from `role`.
- `orgId` in the `role` and in the assignment must be the same.
### Role deletion
- Either the role `name` or `uid` must be provided

View File

@ -0,0 +1,101 @@
+++
title = "Roles"
description = "Understand roles in fine-grained access control"
keywords = ["grafana", "fine-grained-access-control", "roles", "predefined-roles", "built-in-role-assignments", "permissions", "enterprise"]
weight = 105
+++
# Roles
A role represents set of permissions that allow you to perform specific actions on Grafana resources. Refer to [Permissions]({{< relref "./permissions.md" >}}) to understand how permissions work.
There are two types of roles:
- [Predefined roles]({{< relref "./roles.md#predefined-roles" >}}), which provide granular access for specific resources within Grafana and are managed by the Grafana itself.
- [Custom roles]({{< relref "./roles.md#custom-roles.md" >}}), which provide granular access based on the user specified set of permissions.
You can use [Fine-grained access control API]({{< relref "../../http_api/access_control.md" >}}) to list available roles and permissions.
## Role scopes
A role can be either _global_ or _organization local_. _Global_ roles are not mapped to any specific organization and can be reused across multiple organizations, whereas _organization local_ roles are only available for that specific organization.
## Predefined roles
Predefined roles provide convenience and guarantee of consistent behaviour by combining relevant [permissions]({{< relref "./permissions.md" >}}) together. Predefined roles are created and updated by the Grafana, during the startup.
There are few basic rules for predefined roles:
- All predefined roles are _global_ by default
- All predefined roles have a `grafana:roles:` prefix.
- You cant change or delete a predefined role.
Role name | Permissions | Description
--- | --- | ---
grafana:roles:permissions:admin:read | roles:read<br>roles:list<br>roles.builtin:list | Allows to list and get available roles and built-in role assignments.
grafana:roles:permissions:admin:edit | All permissions from `grafana:roles:permissions:admin:read` and <br>roles:write<br>roles:delete<br>roles.builtin:add<br>roles.builtin:remove | Allows every read action and in addition allows to create, change and delete custom roles and create or remove built-in role assignments.
grafana:roles:reporting:admin:read | reports:read<br>reports:send<br>reports.settings:read | Allows to read reports and report settings.
grafana:roles:reporting:admin:edit | All permissions from `grafana:roles:reporting:admin:read` and <br>reports.admin:write<br>reports:delete<br>reports.settings:write | Allows every read action for reports and in addition allows to administer reports.
grafana:roles:users:admin:read | users.authtoken:list<br>users.quotas:list<br>users:read<br>users.teams:read | Allows to list and get users and related information.
grafana:roles:users:admin:edit | All permissions from `grafana:roles:users:admin:read` and <br>users.password:update<br>users:write<br>users:create<br>users:delete<br>users:enable<br>users:disable<br>users.permissions:update<br>users:logout<br>users.authtoken:update<br>users.quotas:update | Allows every read action for users and in addition allows to administer users.
grafana:roles:users:org:read | org.users:read | Allows to get user organizations.
grafana:roles:users:org:edit | All permissions from `grafana:roles:users:org:read` and <br>org.users:add<br>org.users:remove<br>org.users.role:update | Allows every read action for user organizations and in addition allows to administer user organizations.
grafana:roles:ldap:admin:read | ldap.user:read<br>ldap.status:read | Allows to read LDAP information and status.
grafana:roles:ldap:admin:edit | All permissions from `grafana:roles:ldap:admin:read` and <br>ldap.user:sync | Allows every read action for LDAP and in addition allows to administer LDAP.
## Custom roles
Custom roles allow you to manage access to your users the way you want, by mapping [fine-grained permissions]({{< relref "./permissions.md" >}}) to it and creating [built-in role assignments]({{< ref "#built-in-role-assignments.md" >}}).
To create, update or delete a custom role, you can use the [Fine-grained access control API]({{< relref "../../http_api/access_control.md" >}}) or [Grafana Provisioning]({{< relref "./provisioning.md" >}}).
##### Role name
A role's name is intended as a human friendly identifier for the role, helping administrators understand the purpose of a role. The name cannot be longer than 190 characters, and we recommend using ASCII characters.
Role names must be unique within an organization.
Roles with names prefixed by `grafana:roles:` are predefined roles created by Grafana and cannot be created or modified by users.
##### Role version
The version of a role is a positive integer which defines the current version of the role. When updating a role, you can either omit the version field to increment the previous value by 1 or set a new version which must be strictly larger than the previous version for the update to succeed.
##### Permissions
You manage access to Grafana resources by mapping [permissions]({{< relref "./permissions.md" >}}) to roles. You can create and assign roles without any permissions as placeholders.
##### Role UID
Each custom role has a UID defined which is a unique identifier associated with the role allowing you to change or delete the role. You can either generate UID yourself, or let Grafana generate one for you.
The same UID cannot be used for roles in different organizations within the same Grafana instance.
### Create, update and delete roles
You can create, update and delete custom roles by using the [Access Control HTTP API]({{< relref "../../http_api/access_control.md" >}}) or by using [Grafana Provisioning]({{< relref "./provisioning.md" >}}).
By default, Grafana Server Admin has a [built-in role assignment]({{< ref "#built-in-role-assignments" >}}) which allows a user to create, update or delete custom roles.
If a Grafana Server Admin wants to delegate that privilege to other users, they can create a custom role with relevant [permissions]({{< relref "./permissions.md" >}}) and `permissions:delegate` scope will allow those users to manage roles themselves.
Note that you won't be able to create, update or delete a custom role with permissions which you yourself do not have. For example, if the only permission you have is a `users:create`, you won't be able to create a role with other permissions.
## Built-in role assignments
To control what your users can access or not, you can assign or unassign [Custom roles]({{< ref "#custom-roles" >}}) or [Predefined roles]({{< ref "#predefined-roles" >}}) to the existing [Organization roles]({{< relref "../../permissions/organization_roles.md" >}}) or to [Grafana Server Admin]({{< relref "../../permissions/_index.md#grafana-server-admin-role" >}}) role.
These assignments are called built-in role assignments.
During startup, Grafana will create default assignments for you. When you make any changes to the built-on role assignments, Grafana will take them into account and wont overwrite during next start.
### Create and remove built-in role assignments
You can create or remove built-in role assignments using [Fine-grained access control API]({{< relref "../../http_api/access_control.md" >}}) or using [Grafana Provisioning]({{< relref "./provisioning">}}).
### Scope of assignments
A built-in role assignment can be either _global_ or _organization local_. _Global_ assignments are not mapped to any specific organization and will be applied to all organizations, whereas _organization local_ assignments are only applied for that specific organization.
You can only create _organization local_ assignments for _organization local_ roles.
### Default built-in role assignments
Built-in role | Associated role | Description
--- | --- | ---
Grafana Admin | grafana:roles:permissions:admin:edit<br>grafana:roles:permissions:admin:read<br>grafana:roles:reporting:admin:edit<br>grafana:roles:reporting:admin:read<br>grafana:roles:users:admin:edit<br>grafana:roles:users:admin:read<br>grafana:roles:users:org:edit<br>grafana:roles:users:org:read<br>grafana:roles:ldap:admin:edit<br>grafana:roles:ldap:admin:read | Allows access to resources which [Grafana Server Admin]({{< relref "../../permissions/_index.md#grafana-server-admin-role" >}}) has permissions by default.
Admin | grafana:roles:users:org:edit<br>grafana:roles:users:org:read<br>grafana:roles:reporting:admin:edit<br>grafana:roles:reporting:admin:read | Allows access to resource which [Admin]({{< relref "../../permissions/organization_roles.md" >}}) has permissions by default.

View File

@ -0,0 +1,226 @@
+++
title = "Fine-grained access control usage scenarios"
description = "Fine-grained access control usage scenarios"
keywords = ["grafana", "fine-grained-access-control", "roles", "permissions", "fine-grained-access-control-usage", "enterprise"]
weight = 125
+++
# Fine-grained access control usage scenarios
This guide contains several examples and usage scenarios of using fine-grained roles and permissions for controlling access to Grafana resources.
Before you get started, make sure to [enable fine-grained access control]({{< relref "./_index.md#enable-fine-grained-access-control" >}}).
## Check all built-in role assignments
You can use the [Fine-grained access control HTTP API]({{< relref "../../http_api/access_control.md#get-all-built-in-role-assignments" >}}) to see all available built-in role assignments.
The response contains a mapping between one of the organization roles (`Viewer`, `Editor`, `Admin`) or `Grafana Admin` to the custom or predefined roles.
Example request:
```
curl --location --request GET '<grafana_url>/api/access-control/builtin-roles' --header 'Authorization: Basic YWRtaW46cGFzc3dvcmQ='
```
Example response:
```
{
"Admin": [
...
{
"version": 2,
"uid": "qQui_LCMk",
"name": "grafana:roles:users:org:edit",
"description": "Allows every read action for user organizations and in addition allows to administer user organizations.",
"global": true,
"updated": "2021-05-17T20:49:18+02:00",
"created": "2021-05-13T16:24:26+02:00"
},
{
"version": 1,
"uid": "Kz9m_YjGz",
"name": "grafana:roles:reporting:admin:edit",
"description": "Gives access to edit any report or the organization's general reporting settings.",
"global": true,
"updated": "2021-05-13T16:24:26+02:00",
"created": "2021-05-13T16:24:26+02:00"
}
...
],
"Grafana Admin": [
...
{
"version": 2,
"uid": "qQui_LCMk",
"name": "grafana:roles:users:org:edit",
"description": "Allows every read action for user organizations and in addition allows to administer user organizations.",
"global": true,
"updated": "2021-05-17T20:49:18+02:00",
"created": "2021-05-13T16:24:26+02:00"
},
{
"version": 2,
"uid": "ajum_YjGk",
"name": "grafana:roles:users:admin:read",
"description": "Allows to list and get users and related information.",
"global": true,
"updated": "2021-05-17T20:49:17+02:00",
"created": "2021-05-13T16:24:26+02:00"
},
{
"version": 2,
"uid": "K3um_LCMk",
"name": "grafana:roles:users:admin:edit",
"description": "Allows every read action for users and in addition allows to administer users.",
"global": true,
"updated": "2021-05-17T20:49:17+02:00",
"created": "2021-05-13T16:24:26+02:00"
},
...
]
}
```
To see what permissions each of the assigned roles have, you can a [Get a role]({{< relref "../../http_api/access_control.md#get-a-role" >}}) by using an HTTP API.
Example request:
```
curl --location --request GET '<grafana_url>/api/access-control/roles/qQui_LCMk' --header 'Authorization: Basic YWRtaW46cGFzc3dvcmQ='
```
Example response:
```
{
"version": 2,
"uid": "qQui_LCMk",
"name": "grafana:roles:users:org:edit",
"description": "Allows every read action for user organizations and in addition allows to administer user organizations.",
"global": true,
"permissions": [
{
"action": "org.users:add",
"scope": "users:*",
"updated": "2021-05-17T20:49:18+02:00",
"created": "2021-05-17T20:49:18+02:00"
},
{
"action": "org.users:read",
"scope": "users:*",
"updated": "2021-05-17T20:49:18+02:00",
"created": "2021-05-17T20:49:18+02:00"
},
{
"action": "org.users:remove",
"scope": "users:*",
"updated": "2021-05-17T20:49:18+02:00",
"created": "2021-05-17T20:49:18+02:00"
},
{
"action": "org.users.role:update",
"scope": "users:*",
"updated": "2021-05-17T20:49:18+02:00",
"created": "2021-05-17T20:49:18+02:00"
}
],
"updated": "2021-05-17T20:49:18+02:00",
"created": "2021-05-13T16:24:26+02:00"
}
```
## Create your first custom role
You can create your custom role by either using an [HTTP API]({{< relref "../../http_api/access_control.md#create-a-new-custom-role" >}}) or by using [Grafana provisioning]({{< relref "./provisioning.md" >}}).
You can take a look at [actions and scopes]({{< relref "./provisioning.md#action-definitions" >}}) to decide what permissions would you like to map to your role.
Example HTTP request:
```
curl --location --request POST '<grafana_url>/api/access-control/roles/' \
--header 'Authorization: Basic YWRtaW46cGFzc3dvcmQ=' \
--header 'Content-Type: application/json' \
--data-raw '{
"version": 1,
"uid": "jZrmlLCkGksdka",
"name": "custom:users:admin",
"description": "My custom role which gives users permissions to create users",
"global": true,
"permissions": [
{
"action": "users:create"
}
]
}'
```
Example response:
```
{
"version": 1,
"uid": "jZrmlLCkGksdka",
"name": "custom:users:admin",
"description": "My custom role which gives users permissions to create users",
"global": true,
"permissions": [
{
"action": "users:create"
"updated": "2021-05-17T22:07:31.569936+02:00",
"created": "2021-05-17T22:07:31.569935+02:00"
}
],
"updated": "2021-05-17T22:07:31.564403+02:00",
"created": "2021-05-17T22:07:31.564403+02:00"
}
```
Once the custom role is created, you can create a built-in role assignment by using an [HTTP API]({{< relref "../../http_api/access_control.md#create-a-built-in-role-assignment" >}}).
If you created your role using [Grafana provisioning]({{< relref "./provisioning.md" >}}), you can also create the assignment with it.
Example HTTP request:
```
curl --location --request POST '<grafana_url>/api/access-control/builtin-roles' \
--header 'Authorization: Basic YWRtaW46cGFzc3dvcmQ=' \
--header 'Content-Type: application/json' \
--data-raw '{
"roleUid": "jZrmlLCkGksdka",
"builtinRole": "Viewer",
"global": true
}'
```
Example response:
```
{
"message": "Built-in role grant added"
}
```
## Allow Viewers to create reports
In order to create reports, you would need to have `reports.admin:write` permission. By default, Grafana Admin's or organization Admin can create reports as there is a [built-in role assignment]({{< relref "./roles#built-in-role-assignments" >}}) which comes with `reports.admin:write` permission.
If you want your users who have `Viewer` organization role to create reports, you have two options:
1. First option is to create a built-in role assignment and map `grafana:roles:reporting:admin:edit` predefined role to the `Viewer` built-in role. Note that `grafana:roles:reporting:admin:edit` predefined role allows doing more than creating reports. Refer to [predefined roles]({{< relref "./roles.md#predefined-roles" >}}) for full list of permission assignments.
1. Second option is to [create a custom role]({{< ref "#create-your-custom-role" >}}) with `reports.admin:write` permission, and create a built-in role assignment for `Viewer` organization role.
## Prevent Grafana Admin from creating and inviting users
In order to create users, you would need to have `users:create` permission. By default, user with Grafana Admin role can create users as there is a [built-in role assignment]({{< relref "./roles#built-in-role-assignments" >}}) which comes with `users:create` permission.
If you want to prevent Grafana Admin from creating users, you can do the following:
1. [Check all built-in role assignments]({{< ref "#check-all-built-in-role-assignments" >}}) to see what built-in role assignments are available.
1. From built-in role assignments, find the role which gives `users:create` permission. Refer to [predefined roles]({{< relref "./roles.md#predefined-roles" >}}) for full list of permission assignments.
1. Remove the built-in role assignment by using an [Fine-grained access control HTTP API]({{< relref "../../http_api/access_control.md" >}}) or by using [Grafana provisioning]({{< relref "./provisioning" >}}).
## Allow Editors to create new custom roles
By default, Grafana Server Admin is the only user who can create and manage custom roles. If you want your users to do the same, you have two options:
1. First option is to create a built-in role assignment and map `grafana:roles:permissions:admin:edit` and `grafana:roles:permissions:admin:read` predefined roles to the `Editor` built-in role.
1. Second option is to [create a custom role]({{< ref "#create-your-custom-role" >}}) with `roles.builtin:add` and `roles:write` permissions, and create a built-in role assignment for `Editor` organization role.
Note that in any scenario, your `Editor` would be able to create and manage roles only with the permissions they have, or with a subset of them.

View File

@ -2,7 +2,7 @@
title = "Auditing" title = "Auditing"
description = "Auditing" description = "Auditing"
keywords = ["grafana", "auditing", "audit", "logs"] keywords = ["grafana", "auditing", "audit", "logs"]
weight = 700 weight = 1100
+++ +++
# Auditing # Auditing

View File

@ -2,7 +2,7 @@
title = "Data source permissions" title = "Data source permissions"
description = "Grafana Datasource Permissions Guide " description = "Grafana Datasource Permissions Guide "
keywords = ["grafana", "configuration", "documentation", "datasource", "permissions", "users", "teams", "enterprise"] keywords = ["grafana", "configuration", "documentation", "datasource", "permissions", "users", "teams", "enterprise"]
weight = 200 weight = 500
+++ +++
# Data source permissions # Data source permissions

View File

@ -2,7 +2,7 @@
title = "Enhanced LDAP Integration" title = "Enhanced LDAP Integration"
description = "Grafana Enhanced LDAP Integration Guide " description = "Grafana Enhanced LDAP Integration Guide "
keywords = ["grafana", "configuration", "documentation", "ldap", "active directory", "enterprise"] keywords = ["grafana", "configuration", "documentation", "ldap", "active directory", "enterprise"]
weight = 300 weight = 600
+++ +++
# Enhanced LDAP integration # Enhanced LDAP integration

View File

@ -2,7 +2,7 @@
title = "Enterprise configuration" title = "Enterprise configuration"
description = "Enterprise configuration documentation" description = "Enterprise configuration documentation"
keywords = ["grafana", "configuration", "documentation", "enterprise"] keywords = ["grafana", "configuration", "documentation", "enterprise"]
weight = 300 weight = 700
+++ +++
# Grafana Enterprise configuration # Grafana Enterprise configuration

View File

@ -2,7 +2,7 @@
title = "Export dashboard as PDF" title = "Export dashboard as PDF"
description = "" description = ""
keywords = ["grafana", "export", "pdf", "share"] keywords = ["grafana", "export", "pdf", "share"]
weight = 900 weight = 1400
+++ +++
# Export dashboard as PDF # Export dashboard as PDF

View File

@ -2,7 +2,7 @@
title = "Grafana Enterprise license" title = "Grafana Enterprise license"
description = "Enterprise license" description = "Enterprise license"
keywords = ["grafana", "licensing", "enterprise"] keywords = ["grafana", "licensing", "enterprise"]
weight = 100 weight = 10
+++ +++
# Grafana Enterprise license # Grafana Enterprise license

View File

@ -2,7 +2,7 @@
title = "Query caching" title = "Query caching"
description = "Grafana Enterprise data source query caching" description = "Grafana Enterprise data source query caching"
keywords = ["grafana", "plugins", "query", "caching"] keywords = ["grafana", "plugins", "query", "caching"]
weight = 110 weight = 300
+++ +++
# Query caching # Query caching

View File

@ -3,7 +3,7 @@ title = "Reporting"
description = "" description = ""
keywords = ["grafana", "reporting"] keywords = ["grafana", "reporting"]
aliases = ["/docs/grafana/latest/administration/reports"] aliases = ["/docs/grafana/latest/administration/reports"]
weight = 400 weight = 800
+++ +++
# Reporting # Reporting
@ -12,6 +12,9 @@ Reporting allows you to automatically generate PDFs from any of your dashboards
> Only available in Grafana Enterprise v6.4+. > Only available in Grafana Enterprise v6.4+.
> If you have [Fine-grained access Control]({{< relref "../enterprise/access-control/_index.md" >}}) enabled, for some actions you would need to have relevant permissions.
Refer to specific guides to understand what permissions are required.
{{< docs-imagebox img="/img/docs/enterprise/reports_list.png" max-width="500px" class="docs-image--no-shadow" >}} {{< docs-imagebox img="/img/docs/enterprise/reports_list.png" max-width="500px" class="docs-image--no-shadow" >}}
Any changes you make to a dashboard used in a report are reflected the next time the report is sent. For example, if you change the time range in the dashboard, then the time range in the report changes as well. Any changes you make to a dashboard used in a report are reflected the next time the report is sent. For example, if you change the time range in the dashboard, then the time range in the report changes as well.
@ -21,9 +24,13 @@ Any changes you make to a dashboard used in a report are reflected the next time
- SMTP must be configured for reports to be sent. Refer to [SMTP]({{< relref "../administration/configuration.md#smtp" >}}) in [Configuration]({{< relref "../administration/configuration.md" >}}) for more information. - SMTP must be configured for reports to be sent. Refer to [SMTP]({{< relref "../administration/configuration.md#smtp" >}}) in [Configuration]({{< relref "../administration/configuration.md" >}}) for more information.
- The Image Renderer plugin must be installed or the remote rendering service must be set up. Refer to [Image rendering]({{< relref "../administration/image_rendering.md" >}}) for more information. - The Image Renderer plugin must be installed or the remote rendering service must be set up. Refer to [Image rendering]({{< relref "../administration/image_rendering.md" >}}) for more information.
## Access control
When [Fine-grained access control]({{< relref "../enterprise/access-control/_index.md" >}}) is enabled, you need to have the relevant [Permissions]({{< relref "../enterprise/access-control/permissions.md" >}}) to create and manage reports.
## Create or update a report ## Create or update a report
Currently only Organization Admins can create reports. Only organization admins can create reports by default. You can customize who can create reports with [fine-grained access control]({{< relref "../enterprise/access-control/_index.md" >}}).
1. Click on the reports icon in the side menu. The Reports tab allows you to view, create, and update your reports. 1. Click on the reports icon in the side menu. The Reports tab allows you to view, create, and update your reports.
1. Enter report information. All fields are required unless otherwise indicated. 1. Enter report information. All fields are required unless otherwise indicated.

View File

@ -2,7 +2,7 @@
title = "Request security" title = "Request security"
description = "Grafana Enterprise request security" description = "Grafana Enterprise request security"
keywords = ["grafana", "security", "enterprise"] keywords = ["grafana", "security", "enterprise"]
weight = 110 weight = 400
+++ +++
# Request security # Request security

View File

@ -3,7 +3,7 @@ title = "SAML Authentication"
description = "Grafana SAML Authentication" description = "Grafana SAML Authentication"
keywords = ["grafana", "saml", "documentation", "saml-auth"] keywords = ["grafana", "saml", "documentation", "saml-auth"]
aliases = ["/docs/grafana/latest/auth/saml/"] aliases = ["/docs/grafana/latest/auth/saml/"]
weight = 500 weight = 900
+++ +++
# SAML authentication # SAML authentication

View File

@ -3,7 +3,7 @@ title = "Team sync"
description = "Grafana Team Sync" description = "Grafana Team Sync"
keywords = ["grafana", "auth", "documentation"] keywords = ["grafana", "auth", "documentation"]
aliases = ["/docs/grafana/latest/auth/saml/"] aliases = ["/docs/grafana/latest/auth/saml/"]
weight = 600 weight = 1000
+++ +++
# Team sync # Team sync

View File

@ -3,7 +3,7 @@ title = "Usage insights"
description = "Understand how your Grafana instance is used" description = "Understand how your Grafana instance is used"
keywords = ["grafana", "usage-insights", "enterprise"] keywords = ["grafana", "usage-insights", "enterprise"]
aliases = ["/docs/grafana/latest/enterprise/usage-insights/"] aliases = ["/docs/grafana/latest/enterprise/usage-insights/"]
weight = 100 weight = 200
+++ +++
# Usage insights # Usage insights

View File

@ -2,7 +2,7 @@
title = "Vault" title = "Vault"
description = "" description = ""
keywords = ["grafana", "vault", "configuration"] keywords = ["grafana", "vault", "configuration"]
weight = 700 weight = 1200
+++ +++
# Vault integration # Vault integration

View File

@ -3,7 +3,7 @@ title = "White labeling"
description = "Change the look of Grafana to match your corporate brand" description = "Change the look of Grafana to match your corporate brand"
keywords = ["grafana", "white-labeling", "enterprise"] keywords = ["grafana", "white-labeling", "enterprise"]
aliases = ["/docs/grafana/latest/enterprise/white-labeling/"] aliases = ["/docs/grafana/latest/enterprise/white-labeling/"]
weight = 700 weight = 1300
+++ +++
# White labeling # White labeling

View File

@ -25,7 +25,7 @@ If you chose to use InfluxDB Cloud, then you should [download and install the In
## Step 4. Get data into InfluxDB ## Step 4. Get data into InfluxDB
If you downloaded and installed InfluxDB on your local machine, then use the [Quick Start][https://docs.influxdata.com/influxdb/v2.0/write-data/#quick-start-for-influxdb-oss] feature to visualize InfluxDB metrics. If you downloaded and installed InfluxDB on your local machine, then use the [Quick Start](https://docs.influxdata.com/influxdb/v2.0/write-data/#quick-start-for-influxdb-oss) feature to visualize InfluxDB metrics.
If you are using the cloud account, then the wizards will guide you through the initial process. For more information, refer to [Configure Telegraf](https://docs.influxdata.com/influxdb/cloud/write-data/no-code/use-telegraf/#configure-telegraf). If you are using the cloud account, then the wizards will guide you through the initial process. For more information, refer to [Configure Telegraf](https://docs.influxdata.com/influxdb/cloud/write-data/no-code/use-telegraf/#configure-telegraf).

View File

@ -22,7 +22,7 @@ To log in to Grafana for the first time:
1. Open your web browser and go to http://localhost:3000/. The default HTTP port that Grafana listens to is `3000` unless you have configured a different port. 1. Open your web browser and go to http://localhost:3000/. The default HTTP port that Grafana listens to is `3000` unless you have configured a different port.
1. On the login page, enter `admin` for username and password. 1. On the login page, enter `admin` for username and password.
1. Click **Log In**. If login is successful, then you will see a prompt to change the password. 1. Click **Log in**. If login is successful, then you will see a prompt to change the password.
1. Click **OK** on the prompt, then change your password. 1. Click **OK** on the prompt, then change your password.
> **Note:** We strongly recommend that you follow Grafana's best practices and change the default administrator password. Don't forget to record your credentials! > **Note:** We strongly recommend that you follow Grafana's best practices and change the default administrator password. Don't forget to record your credentials!
@ -31,8 +31,9 @@ To log in to Grafana for the first time:
To create your first dashboard: To create your first dashboard:
1. Click the **+** icon on the left panel, select **Create Dashboard**, and then click **Add an empty panel**. 1. Click the **+** icon on the side menu.
1. In the New Dashboard/Edit Panel view, go to the **Query** tab. 1. On the dashboard, click **Add an empty panel**.
1. In the New dashboard/Edit panel view, go to the **Query** tab.
1. Configure your [query]({{< relref "../panels/queries.md" >}}) by selecting ``-- Grafana --`` from the [data source selector]({{< relref "../panels/queries.md/#data-source-selector" >}}). This generates the Random Walk dashboard. 1. Configure your [query]({{< relref "../panels/queries.md" >}}) by selecting ``-- Grafana --`` from the [data source selector]({{< relref "../panels/queries.md/#data-source-selector" >}}). This generates the Random Walk dashboard.
1. Click the **Save** icon in the top right corner of your screen to save the dashboard. 1. Click the **Save** icon in the top right corner of your screen to save the dashboard.
1. Add a descriptive name, and then click **Save**. 1. Add a descriptive name, and then click **Save**.

View File

@ -37,6 +37,7 @@ dashboards, creating users and updating data sources.
Grafana Enterprise includes all of the Grafana OSS APIs as well as those that follow: Grafana Enterprise includes all of the Grafana OSS APIs as well as those that follow:
- [Fine-Grained Access Control API]({{< relref "access_control.md" >}})
- [Data Source Permissions API]({{< relref "datasource_permissions.md" >}}) - [Data Source Permissions API]({{< relref "datasource_permissions.md" >}})
- [External Group Sync API]({{< relref "external_group_sync.md" >}}) - [External Group Sync API]({{< relref "external_group_sync.md" >}})
- [License API]({{< relref "licensing.md" >}}) - [License API]({{< relref "licensing.md" >}})

View File

@ -0,0 +1,584 @@
+++
title = "Fine-grained access control HTTP API "
description = "Fine-grained access control API"
keywords = ["grafana", "http", "documentation", "api", "fine-grained-access-control", "acl", "enterprise"]
aliases = ["/docs/grafana/latest/http_api/accesscontrol/"]
+++
# Fine-grained access control API
> Fine-grained access control API is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "../enterprise" >}}).
The API can be used to create, update, get and list roles, and create or remove built-in role assignments.
To use the API, you would need to [enable fine-grained access control]({{< relref "../enterprise/access-control/_index.md#enable-fine-grained-access-control" >}}).
The API does not currently work with an API Token. So in order to use these API endpoints you will have to use [Basic auth]({{< relref "./auth/#basic-auth" >}}).
## Get status
`GET /api/access-control/status`
Returns an indicator to check if fine-grained access control is enabled or not.
### Required permissions
Action | Scope
--- | --- |
status:accesscontrol | services:accesscontrol
#### Example request
```http
GET /api/access-control/check
Accept: application/json
Content-Type: application/json
```
#### Example response
```http
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
{
"enabled": true
}
```
#### Status codes
Code | Description
--- | --- |
200 | Returned a flag indicating if the fine-grained access control is enabled or no.
403 | Access denied
404 | Not found, an indication that fine-grained access control is not available at all.
500 | Unexpected error. Refer to body and/or server logs for more details.
## Create and manage custom roles
### Get all roles
`GET /api/access-control/roles`
Gets all existing roles. The response contains all global and organization local roles, for the organization which user is signed in.
Refer to the [Role scopes]({{< relref "../enterprise/access-control/roles.md#built-in-role-assignments" >}}) for more information.
#### Required permissions
Action | Scope
--- | --- |
roles:list | roles:*
#### Example request
```http
GET /api/access-control/roles
Accept: application/json
Content-Type: application/json
```
#### Example response
```http
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
[
{
"version": 1,
"uid": "Kz9m_YjGz",
"name": "grafana:roles:reporting:admin:edit",
"description": "Gives access to edit any report or the organization's general reporting settings.",
"global": true,
"updated": "2021-05-13T16:24:26+02:00",
"created": "2021-05-13T16:24:26+02:00"
},
{
"version": 5,
"uid": "vi9mlLjGz",
"name": "grafana:roles:permissions:admin:read",
"description": "Gives access to read and list roles and permissions, as well as built-in role assignments.",
"global": true,
"updated": "2021-05-13T22:41:49+02:00",
"created": "2021-05-13T16:24:26+02:00"
}
]
```
#### Status codes
Code | Description
--- | --- |
200 | Global and organization local roles are returned.
403 | Access denied
500 | Unexpected error. Refer to body and/or server logs for more details.
### Get a role
`GET /api/access-control/roles/:uid`
Get a role for the given UID.
#### Required permissions
Action | Scope
--- | --- |
roles:read | roles:*
#### Example request
```http
GET /api/access-control/roles/PYnDO3rMk
Accept: application/json
Content-Type: application/json
```
#### Example response
```http
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
{
"version": 2,
"uid": "jZrmlLCGk",
"name": "grafana:roles:permissions:admin:edit",
"description": "Gives access to create, update and delete roles, as well as manage built-in role assignments.",
"global": true,
"permissions": [
{
"action": "roles:delete",
"scope": "permissions:delegate",
"updated": "2021-05-13T16:24:26+02:00",
"created": "2021-05-13T16:24:26+02:00"
},
{
"action": "roles:list",
"scope": "roles:*",
"updated": "2021-05-13T16:24:26+02:00",
"created": "2021-05-13T16:24:26+02:00"
}
],
"updated": "2021-05-13T16:24:26+02:00",
"created": "2021-05-13T16:24:26+02:00"
}
```
#### Status codes
Code | Description
--- | --- |
200 | Role is returned.
403 | Access denied
500 | Unexpected error. Refer to body and/or server logs for more details.
### Create a new custom role
`POST /api/access-control/roles`
Creates a new custom role and maps given permissions to that role. Note that roles with the same prefix as [Predefined Roles]({{< relref "../enterprise/access-control/roles.md" >}}) can't be created.
#### Required permissions
`permission:delegate` scope ensures that users can only create custom roles with the same, or a subset of permissions which the user has.
For example, if a user does not have required permissions for creating users, they won't be able to create a custom role which allows to do that. This is done to prevent escalation of privileges.
Action | Scope
--- | --- |
roles:write | permissions:delegate
#### Example request
```http
POST /api/access-control/roles
Accept: application/json
Content-Type: application/json
{
"version": 1,
"uid": "jZrmlLCGka",
"name": "custom:delete:roles",
"description": "My custom role which gives users permissions to delete roles",
"global": true,
"permissions": [
{
"action": "roles:delete",
"scope": "permissions:delegate"
}
]
}
```
#### JSON body schema
Field Name | Date Type | Required | Description
--- | --- | --- | ---
uid | string | No | UID of the role. If not present, the UID will be automatically created for you and returned in response. Refer to the [Custom roles]({{< relref "../enterprise/access-control/roles.md#custom-roles" >}}) for more information.
global | boolean | No | A flag indicating if the role is global or not. If set to `false`, the default org ID of the authenticated user will be used from the request. Refer to the [Role scopes]({{< relref "../enterprise/access-control/roles.md#role-scopes" >}}) for more information.
version | number | No | Version of the role. If not present, version 0 will be assigned to the role and returned in the response. Refer to the [Custom roles]({{< relref "../enterprise/access-control/roles.md#custom-roles" >}}) for more information.
name | string | Yes | Name of the role. Refer to [Custom roles]({{< relref "../enterprise/access-control/roles.md#custom-roles" >}}) for more information.
description | string | No | Description of the role.
permissions | Permission | No | If not present, the role will be created without any permissions.
**Permission**
Field Name | Data Type | Required | Description
--- | --- | --- | ---
action | string | Yes | Refer to [Permissions]({{< relref "../enterprise/access-control/permissions.md" >}}) for full list of available actions.
scope | string | No | If not present, no scope will be mapped to the permission. Refer to [Permissions]({{< relref "../enterprise/access-control/permissions.md#scope-definitions" >}}) for full list of available scopes.
#### Example response
```http
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
{
"version": 2,
"uid": "jZrmlLCGka",
"name": "custom:delete:create:roles",
"description": "My custom role which gives users permissions to delete and create roles",
"global": true,
"permissions": [
{
"action": "roles:delete",
"scope": "permissions:delegate",
"updated": "2021-05-13T23:19:46+02:00",
"created": "2021-05-13T23:19:46+02:00"
}
],
"updated": "2021-05-13T23:20:51.416518+02:00",
"created": "2021-05-13T23:19:46+02:00"
}
```
#### Status codes
Code | Description
--- | --- |
200 | Role is updated.
400 | Bad request (invalid json, missing content-type, missing or invalid fields, etc.).
403 | Access denied
500 | Unexpected error. Refer to body and/or server logs for more details.
### Update a custom role
`PUT /api/access-control/roles/:uid`
Update the role with the given UID, and it's permissions with the given UID. The operation is idempotent and all permissions of the role will be replaced with what is in the request. You would need to increment the version of the role with each update, otherwise the request will fail.
#### Required permissions
`permission:delegate` scope ensures that users can only update custom roles with the same, or a subset of permissions which the user has.
For example, if a user does not have required permissions for creating users, they won't be able to update a custom role which allows to do that. This is done to prevent escalation of privileges.
Action | Scope
--- | --- |
roles:write | permissions:delegate
#### Example request
```http
PUT /api/access-control/roles/jZrmlLCGka
Accept: application/json
Content-Type: application/json
{
"version": 2,
"name": "custom:delete:create:roles",
"description": "My custom role which gives users permissions to delete and create roles",
"permissions": [
{
"action": "roles:delete",
"scope": "permissions:delegate"
},
{
"action": "roles:create",
"scope": "permissions:delegate"
}
]
}
```
#### JSON body schema
Field Name | Data Type | Required | Description
--- | --- | --- | ---
version | number | Yes | Version of the role. Must be incremented for update to work.
name | string | Yes | Name of the role.
description | string | No | Description of the role.
permissions | List of Permissions | No | The full list of permissions the role should have after the update.
**Permission**
Field Name | Data Type | Required | Description
--- | --- | --- | ---
action | string | Yes | Refer to [Permissions]({{< relref "../enterprise/access-control/permissions.md" >}}) for full list of available actions.
scope | string | No | If not present, no scope will be mapped to the permission. Refer to [Permissions]({{< relref "../enterprise/access-control/permissions.md#scope-definitions" >}}) for full list of available scopes.
#### Example response
```http
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
{
"version": 3,
"name": "custom:delete:create:roles",
"description": "My custom role which gives users permissions to delete and create roles",
"permissions": [
{
"action": "roles:delete",
"scope": "permissions:delegate",
"updated": "2021-05-13T23:19:46.546146+02:00",
"created": "2021-05-13T23:19:46.546146+02:00"
},
{
"action": "roles:create",
"scope": "permissions:delegate",
"updated": "2021-05-13T23:19:46.546146+02:00",
"created": "2021-05-13T23:19:46.546146+02:00"
}
],
"updated": "2021-05-13T23:19:46.540987+02:00",
"created": "2021-05-13T23:19:46.540986+02:00"
}
```
#### Status codes
Code | Description
--- | --- |
200 | Role is updated.
400 | Bad request (invalid json, missing content-type, missing or invalid fields, etc.).
403 | Access denied
404 | Role was not found to update.
500 | Unexpected error. Refer to body and/or server logs for more details.
### Delete a custom role
`DELETE /api/access-control/roles/:uid?force=false`
Delete a role with the given UID, and it's permissions. If the role is assigned to a built-in role, the deletion operation will fail, unless `force` query param is set to `true`, and in that case all assignments will also be deleted.
#### Required permissions
`permission:delegate` scope ensures that users can only delete a custom role with the same, or a subset of permissions which the user has.
For example, if a user does not have required permissions for creating users, they won't be able to delete a custom role which allows to do that.
Action | Scope
--- | --- |
roles:delete | permissions:delegate
#### Example request
```http
DELETE /api/access-control/roles/jZrmlLCGka?force=true
Accept: application/json
```
#### Query parameters
Param | Type | Required | Description
--- | --- | --- | ---
force | boolean | No | When set to `true`, the role will be deleted with all it's assignments.
#### Example response
```http
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
{
"message": "Role deleted"
}
```
#### Status codes
Code | Description
--- | --- |
200 | Role is deleted.
400 | Bad request (invalid json, missing content-type, missing or invalid fields, etc.).
403 | Access denied
500 | Unexpected error. Refer to body and/or server logs for more details.
## Create and remove built-in role assignments
API set allows to create or remove [built-in role assignments]({{< relref "../enterprise/access-control/roles.md#built-in-role-assignments" >}}) and list current assignments.
### Get all built-in role assignments
`GET /api/access-control/builtin-roles`
Gets all built-in role assignments.
#### Required permissions
Action | Scope
--- | --- |
roles.builtin:list | roles:*
#### Example request
```http
GET /api/access-control/builtin-roles
Accept: application/json
Content-Type: application/json
```
#### Example response
```http
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
{
"Admin": [
{
"version": 1,
"uid": "qQui_LCMk",
"name": "grafana:roles:users:org:edit",
"description": "",
"global": true,
"updated": "2021-05-13T16:24:26+02:00",
"created": "2021-05-13T16:24:26+02:00"
},
{
"version": 1,
"uid": "PeXmlYjMk",
"name": "grafana:roles:users:org:read",
"description": "",
"global": true,
"updated": "2021-05-13T16:24:26+02:00",
"created": "2021-05-13T16:24:26+02:00"
}
],
"Grafana Admin": [
{
"version": 1,
"uid": "qQui_LCMk",
"name": "grafana:roles:users:org:edit",
"description": "",
"global": true,
"updated": "2021-05-13T16:24:26+02:00",
"created": "2021-05-13T16:24:26+02:00"
}
]
}
```
#### Status codes
Code | Description
--- | --- |
200 | Built-in role assignments are returned.
403 | Access denied
500 | Unexpected error. Refer to body and/or server logs for more details.
### Create a built-in role assignment
`POST /api/access-control/builtin-roles`
Creates a new built-in role assignment.
#### Required permissions
`permission:delegate` scope ensures that users can only create built-in role assignments with the roles which have same, or a subset of permissions which the user has.
For example, if a user does not have required permissions for creating users, they won't be able to create a built-in role assignment which will allow to do that. This is done to prevent escalation of privileges.
Action | Scope
--- | --- |
roles.builtin:add | permissions:delegate
#### Example request
```http
POST /api/access-control/builtin-roles
Accept: application/json
Content-Type: application/json
{
"roleUid": "LPMGN99Mk",
"builtinRole": "Grafana Admin",
"global": false
}
```
#### JSON body schema
Field Name | Date Type | Required | Description
--- | --- | --- | ---
roleUid | string | Yes | UID of the role.
builtinRole | boolean | Yes | Can be one of `Viewer`, `Editor`, `Admin` or `Grafana Admin`.
global | boolean | No | A flag indicating if the assignment is global or not. If set to `false`, the default org ID of the authenticated user will be used from the request to create organization local assignment. Refer to the [Built-in role assignments]({{< relref "../enterprise/access-control/roles.md#built-in-role-assignments" >}}) for more information.
#### Example response
```http
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
{
"message": "Built-in role grant added"
}
```
#### Status codes
Code | Description
--- | --- |
200 | Role was assigned to built-in role.
400 | Bad request (invalid json, missing content-type, missing or invalid fields, etc.).
403 | Access denied
404 | Role not found
500 | Unexpected error. Refer to body and/or server logs for more details.
### Remove a built-in role assignment
`DELETE /api/access-control/builtin-roles/:builtinRole/roles/:roleUID`
Deletes a built-in role assignment (for one of _Viewer_, _Editor_, _Admin_, or _Grafana Admin_) to the role with the provided UID.
#### Required permissions
`permission:delegate` scope ensures that users can only remove built-in role assignments with the roles which have same, or a subset of permissions which the user has.
For example, if a user does not have required permissions for creating users, they won't be able to remove a built-in role assignment which allows to do that.
Action | Scope
--- | --- |
roles.builtin:remove | permissions:delegate
#### Example request
```http
DELETE /api/access-control/builtin-roles/Grafana%20Admin/roles/LPMGN99Mk?global=false
Accept: application/json
```
#### Query parameters
Param | Type | Required | Description
--- | --- | --- | ---
global | boolean | No | A flag indicating if the assignment is global or not. If set to `false`, the default org ID of the authenticated user will be used from the request to remove assignment. Refer to the [Built-in role assignments]({{< relref "../enterprise/access-control/roles.md#built-in-role-assignments" >}}) for more information.
#### Example response
```http
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
{
"message": "Built-in role grant removed"
}
```
#### Status codes
Code | Description
--- | --- |
200 | Role was unassigned from built-in role.
400 | Bad request (invalid json, missing content-type, missing or invalid fields, etc.).
403 | Access denied
404 | Role not found.
500 | Unexpected error. Refer to body and/or server logs for more details.

View File

@ -11,6 +11,9 @@ The Admin HTTP API does not currently work with an API Token. API Tokens are cur
the permission of server admin, only users can be given that permission. So in order to use these API calls you will have to use Basic Auth and the Grafana user the permission of server admin, only users can be given that permission. So in order to use these API calls you will have to use Basic Auth and the Grafana user
must have the Grafana Admin permission. (The default admin user is called `admin` and has permission to use this API.) must have the Grafana Admin permission. (The default admin user is called `admin` and has permission to use this 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.
## Settings ## Settings
`GET /api/admin/settings` `GET /api/admin/settings`
@ -209,6 +212,14 @@ Content-Type: application/json
Create new user. Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation. Create new user. Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation.
#### Required permissions
See note in the [introduction]({{< ref "#admin-api" >}}) for an explanation.
Action | Scope
--- | --- |
users:create | n/a
**Example Request**: **Example Request**:
```http ```http
@ -243,6 +254,14 @@ Content-Type: application/json
Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation. Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation.
Change password for a specific user. Change password for a specific user.
#### Required permissions
See note in the [introduction]({{< ref "#admin-api" >}}) for an explanation.
Action | Scope
--- | --- |
users.password:update | global:users:*
**Example Request**: **Example Request**:
```http ```http
@ -268,6 +287,14 @@ Content-Type: application/json
Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation. Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation.
#### Required permissions
See note in the [introduction]({{< ref "#admin-api" >}}) for an explanation.
Action | Scope
--- | --- |
users.permissions:update | global:users:*
**Example Request**: **Example Request**:
```http ```http
@ -293,6 +320,14 @@ Content-Type: application/json
Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation. Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation.
#### Required permissions
See note in the [introduction]({{< ref "#admin-api" >}}) for an explanation.
Action | Scope
--- | --- |
users:delete | global:users:*
**Example Request**: **Example Request**:
```http ```http
@ -353,6 +388,14 @@ Return a list of all auth tokens (devices) that the user currently have logged i
Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation. Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation.
#### Required permissions
See note in the [introduction]({{< ref "#admin-api" >}}) for an explanation.
Action | Scope
--- | --- |
users.authtoken:list | global:users:*
**Example Request**: **Example Request**:
```http ```http
@ -404,6 +447,14 @@ and will be required to authenticate again upon next activity.
Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation. Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation.
#### Required permissions
See note in the [introduction]({{< ref "#admin-api" >}}) for an explanation.
Action | Scope
--- | --- |
users.authtoken:update | global:users:*
**Example Request**: **Example Request**:
```http ```http
@ -436,6 +487,14 @@ and will be required to authenticate again upon next activity.
Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation. Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation.
#### Required permissions
See note in the [introduction]({{< ref "#admin-api" >}}) for an explanation.
Action | Scope
--- | --- |
users.logout | global:users:*
**Example Request**: **Example Request**:
```http ```http
@ -465,12 +524,22 @@ Content-Type: application/json
`POST /api/admin/provisioning/notifications/reload` `POST /api/admin/provisioning/notifications/reload`
`POST /api/admin/provisioning/accesscontrol/reload`
Reloads the provisioning config files for specified type and provision entities again. It won't return Reloads the provisioning config files for specified type and provision entities again. It won't return
until the new provisioned entities are already stored in the database. In case of dashboards, it will stop until the new provisioned entities are already stored in the database. In case of dashboards, it will stop
polling for changes in dashboard files and then restart it with new configurations after returning. polling for changes in dashboard files and then restart it with new configurations after returning.
Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation. Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation.
#### Required permissions
See note in the [introduction]({{< ref "#admin-api" >}}) for an explanation.
Action | Scope | Provision entity
--- | --- | ---
provisioning:reload | service:accesscontrol | accesscontrol
**Example Request**: **Example Request**:
```http ```http

View File

@ -5,13 +5,15 @@ keywords = ["grafana", "http", "documentation", "api", "organization"]
aliases = ["/docs/grafana/latest/http_api/organization/"] aliases = ["/docs/grafana/latest/http_api/organization/"]
+++ +++
# Organization API # Organization API
The Organization HTTP API is divided in two resources, `/api/org` (current organization) The Organization HTTP API is divided in two resources, `/api/org` (current organization)
and `/api/orgs` (admin organizations). One big difference between these are that and `/api/orgs` (admin organizations). One big difference between these are that
the admin of all organizations API only works with basic authentication, see [Admin Organizations API](#admin-organizations-api) for more information. the admin of all organizations API only works with basic authentication, see [Admin Organizations API](#admin-organizations-api) for more information.
> 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.
## Current Organization API ## Current Organization API
### Get current Organization ### Get current Organization
@ -46,6 +48,14 @@ Content-Type: application/json
Returns all org users within the current organization. Returns all org users within the current organization.
Accessible to users with org admin role. Accessible to users with org admin role.
#### Required permissions
See note in the [introduction]({{< ref "#organization-api" >}}) for an explanation.
Action | Scope
--- | --- |
org.users:read | users:*
**Example Request**: **Example Request**:
```http ```http
@ -112,6 +122,14 @@ Content-Type: application/json
`PATCH /api/org/users/:userId` `PATCH /api/org/users/:userId`
#### Required permissions
See note in the [introduction]({{< ref "#organization-api" >}}) for an explanation.
Action | Scope
--- | --- |
org.users.role:update | users:*
**Example Request**: **Example Request**:
```http ```http
@ -138,6 +156,14 @@ Content-Type: application/json
`DELETE /api/org/users/:userId` `DELETE /api/org/users/:userId`
#### Required permissions
See note in the [introduction]({{< ref "#organization-api" >}}) for an explanation.
Action | Scope
--- | --- |
org.users:remove | users:*
**Example Request**: **Example Request**:
```http ```http
@ -188,6 +214,14 @@ Content-Type: application/json
Adds a global user to the current organization. Adds a global user to the current organization.
#### Required permissions
See note in the [introduction]({{< ref "#organization-api" >}}) for an explanation.
Action | Scope
--- | --- |
org.users:add | users:*
**Example Request**: **Example Request**:
```http ```http
@ -407,6 +441,14 @@ Content-Type: application/json
Only works with Basic Authentication (username and password), see [introduction](#admin-organizations-api). Only works with Basic Authentication (username and password), see [introduction](#admin-organizations-api).
#### Required permissions
See note in the [introduction]({{< ref "#organization-api" >}}) for an explanation.
Action | Scope
--- | --- |
org.users:read | users:*
**Example Request**: **Example Request**:
```http ```http
@ -440,6 +482,14 @@ Content-Type: application/json
Only works with Basic Authentication (username and password), see [introduction](#admin-organizations-api). Only works with Basic Authentication (username and password), see [introduction](#admin-organizations-api).
#### Required permissions
See note in the [introduction]({{< ref "#organization-api" >}}) for an explanation.
Action | Scope
--- | --- |
org.users:add | users:*
**Example Request**: **Example Request**:
```http ```http
@ -468,6 +518,14 @@ Content-Type: application/json
Only works with Basic Authentication (username and password), see [introduction](#admin-organizations-api). Only works with Basic Authentication (username and password), see [introduction](#admin-organizations-api).
#### Required permissions
See note in the [introduction]({{< ref "#organization-api" >}}) for an explanation.
Action | Scope
--- | --- |
org.users.role:update | users:*
**Example Request**: **Example Request**:
```http ```http
@ -495,6 +553,14 @@ Content-Type: application/json
Only works with Basic Authentication (username and password), see [introduction](#admin-organizations-api). Only works with Basic Authentication (username and password), see [introduction](#admin-organizations-api).
#### Required permissions
See note in the [introduction]({{< ref "#organization-api" >}}) for an explanation.
Action | Scope
--- | --- |
org.users:remove | users:*
**Example Request**: **Example Request**:
```http ```http

View File

@ -11,6 +11,8 @@ This API allows you to interact programmatically with the [Reporting]({{< relref
> Reporting is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "../enterprise" >}}). > Reporting is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "../enterprise" >}}).
> If you 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.
## Send a report ## Send a report
@ -22,6 +24,14 @@ This API allows you to interact programmatically with the [Reporting]({{< relref
Generate and send a report. This API waits for the report to be generated before returning. We recommend that you set the client's timeout to at least 60 seconds. Generate and send a report. This API waits for the report to be generated before returning. We recommend that you set the client's timeout to at least 60 seconds.
#### Required permissions
See note in the [introduction]({{< ref "#reporting-api" >}}) for an explanation.
Action | Scope
--- | --- |
reports:send | n/a
### Example request ### Example request
```http ```http
@ -63,4 +73,4 @@ Code | Description
401 | Authentication failed, refer to [Authentication API]({{< relref "../http_api/auth.md" >}}). 401 | Authentication failed, refer to [Authentication API]({{< relref "../http_api/auth.md" >}}).
403 | User is authenticated but is not authorized to generate the report. 403 | User is authenticated but is not authorized to generate the report.
404 | Report not found. 404 | Report not found.
500 | Unexpected error or server misconfiguration. Refer to body and/or server logs for more details. 500 | Unexpected error or server misconfiguration. Refer to server logs for more details.

View File

@ -5,12 +5,23 @@ keywords = ["grafana", "http", "documentation", "api", "user"]
aliases = ["/docs/grafana/latest/http_api/user/"] aliases = ["/docs/grafana/latest/http_api/user/"]
+++ +++
# User HTTP resources / actions # User 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 Users ## Search Users
`GET /api/users?perpage=10&page=1` `GET /api/users?perpage=10&page=1`
#### Required permissions
See note in the [introduction]({{< ref "#user-api" >}}) for an explanation.
Action | Scope
--- | --- |
users:read | global:users:*
**Example Request**: **Example Request**:
```http ```http
@ -58,6 +69,14 @@ Content-Type: application/json
`GET /api/users/search?perpage=10&page=1&query=mygraf` `GET /api/users/search?perpage=10&page=1&query=mygraf`
#### Required permissions
See note in the [introduction]({{< ref "#user-api" >}}) for an explanation.
Action | Scope
--- | --- |
users:read | global:users:*
**Example Request**: **Example Request**:
```http ```http
@ -111,6 +130,14 @@ Content-Type: application/json
`GET /api/users/:id` `GET /api/users/:id`
#### Required permissions
See note in the [introduction]({{< ref "#user-api" >}}) for an explanation.
Action | Scope
--- | --- |
users:read | users:*
**Example Request**: **Example Request**:
```http ```http
@ -148,6 +175,14 @@ Content-Type: application/json
`GET /api/users/lookup?loginOrEmail=user@mygraf.com` `GET /api/users/lookup?loginOrEmail=user@mygraf.com`
#### Required permissions
See note in the [introduction]({{< ref "#user-api" >}}) for an explanation.
Action | Scope
--- | --- |
users:read | global:users:*
**Example Request using the email as option**: **Example Request using the email as option**:
```http ```http
@ -195,6 +230,14 @@ Content-Type: application/json
`PUT /api/users/:id` `PUT /api/users/:id`
#### Required permissions
See note in the [introduction]({{< ref "#user-api" >}}) for an explanation.
Action | Scope
--- | --- |
users:write | users:*
**Example Request**: **Example Request**:
```http ```http
@ -226,6 +269,14 @@ Content-Type: application/json
`GET /api/users/:id/orgs` `GET /api/users/:id/orgs`
#### Required permissions
See note in the [introduction]({{< ref "#user-api" >}}) for an explanation.
Action | Scope
--- | --- |
users:read | users:*
**Example Request**: **Example Request**:
```http ```http
@ -256,6 +307,14 @@ Content-Type: application/json
`GET /api/users/:id/teams` `GET /api/users/:id/teams`
#### Required permissions
See note in the [introduction]({{< ref "#user-api" >}}) for an explanation.
Action | Scope
--- | --- |
users.teams:read | users:*
**Example Request**: **Example Request**:
```http ```http

View File

@ -8,7 +8,7 @@ weight = 300
# Panel links # Panel links
Each panel can have its own set of links that are shown in the upper left corner of the panel. You can link to any available URL, including dashboards, panels, or external sites. You can even control the time range to ensure the user is zoomed in on the right data in Grafana. {{< docs/shared "panels/panel-links-intro.md" >}}
Click the icon on the top left corner of a panel to see available panel links. Click the icon on the top left corner of a panel to see available panel links.

View File

@ -9,6 +9,8 @@ Grafana offers several options for grouping users. Each level has different tool
One of the most important user management tasks is assigning roles, which govern what [permissions]({{< relref "../permissions/_index.md" >}}) a user has. The correct permissions ensure that users have access to only the resources they need. One of the most important user management tasks is assigning roles, which govern what [permissions]({{< relref "../permissions/_index.md" >}}) a user has. The correct permissions ensure that users have access to only the resources they need.
> Refer to [Fine-grained access Control]({{< relref "../enterprise/access-control/_index.md" >}}) in Grafana Enterprise to understand how you can manage users with fine-grained permissions.
## Server ## Server
The highest and broadest level of user group in Grafana is the server. Every user with an account in a Grafana instance is a member of the server group. The highest and broadest level of user group in Grafana is the server. Every user with an account in a Grafana instance is a member of the server group.

View File

@ -1,58 +1,24 @@
+++ +++
draft = "true" draft = "true"
description = "A topic to collect my thoughts and plans for Grafana 8.0. Can delete after that."
+++ +++
Task: Add panel
Default visualization: Time series
- change if you want to, or go to query
Get data into your panel
- Select data source
- Add query and/or expression
- Transform data
- Troubleshoot
- Query options
- Inspect query
- Table view
Make it look good
- Change the visualization
- Panel options
- Visualization-specific options
- Thresholds
- Standard options
- Value mappings
- Data links
- Overrides
Next steps
- Set up alert
TOC TOC
# General # General
Add a panel Add a panel
Panel editor Panel editor
# Work with your metrics
Queries Queries
- Share query results - Share query results
- Mixed data source queries - Mixed data source queries (not yet written)
Expressions Expressions (beta)
Transformations Transformations
Inspect a panel Inspect a panel
# Adjust appearance
Panel options Panel options
Visualization-specific settings (Visualizations) Visualization-specific settings (Visualizations)
Thresholds Thresholds
Value mappings Value mappings
Data links (can be for all fields or one) Data links (can be for all fields or one)
Visualizations > specific options
Overrides Overrides

View File

@ -16,30 +16,34 @@ Panels allow you to show your data in visual form. This topic walks you through
1. Click **Add an empty panel**. 1. Click **Add an empty panel**.
Grafana creates an empty graph panel with your default data source selected. Grafana creates an empty time series panel with your default data source selected.
## 2. Edit panel settings ## 2. Write a query
While not required, we recommend that you add a helpful title and description to your panel. You can use [variables you have defined]({{< relref "../variables/_index.md" >}}) in either field, but not [global variables]({{< relref "../variables/variable-types/global-variables.md" >}}).
![](/img/docs/panels/panel-settings-7-0.png)
**Panel title -** Text entered in this field is displayed at the top of your panel in the panel editor and in the dashboard.
**Description -** Text entered in this field is displayed in a tooltip in the upper left corner of the panel. Write a description of the panel and the data you are displaying. Pretend you are explaining it to a new user six months from now, when it is no longer fresh in your mind. Future editors (possibly yourself) will thank you.
## 3. Write a query
Each panel needs at least one query to display a visualization. You write queries in the Query tab of the panel editor. For more information about the Query tab, refer to [Queries]({{< relref "queries.md" >}}). Each panel needs at least one query to display a visualization. You write queries in the Query tab of the panel editor. For more information about the Query tab, refer to [Queries]({{< relref "queries.md" >}}).
1. Choose a data source. In the first line of the Query tab, click the drop-down list to see all available data sources. This list includes all data sources you added. Refer to [Add a data source]({{< relref "../datasources/add-a-data-source.md" >}}) if you need instructions. 1. Choose a data source. In the first line of the Query tab, click the drop-down list to see all available data sources. This list includes all data sources you added. Refer to [Add a data source]({{< relref "../datasources/add-a-data-source.md" >}}) if you need instructions.
1. Write or construct a query in the query language of your data source. Options will vary. Refer to your specific [data source documentation]({{< relref "../datasources/_index.md" >}}) for specific guidelines. 1. Write or construct a query in the query language of your data source. Options will vary. Refer to your specific [data source documentation]({{< relref "../datasources/_index.md" >}}) for specific guidelines.
## 4. Choose a visualization type ## 3. Choose a visualization type
In the Visualization section of the Panel tab, click a visualization type. Grafana displays a preview of your query results with that visualization applied. In the Visualization list, click a visualization type. Grafana displays a preview of your query results with that visualization applied.
For more information about individual visualizations, refer to [Visualizations]({{< relref "visualizations/_index.md" >}}). ![](/img/docs/panel-editor/select-visualization-8-0.png)
For more information about individual visualizations, refer to [Visualizations options]({{< relref "visualizations/_index.md" >}}).
## 4. (Optional) Edit panel settings
While not required, most visualizations need some adjustment before they properly display the information that you need. Options are defined in the linked topics below.
- [Panel options]({{< relref "./panel-options.md" >}})
- [Visualization-specific options]({{< relref "./visualizations/_index.md" >}})
- [Standard options]({{< relref "./standard-options.md" >}})
- [Thresholds]({{< relref "./thresholds.md" >}})
- [Value mappings]({{< relref "./value-mappings.md" >}})
- [Data links]({{< relref "../linking/data-links.md" >}})
- [Override fields]({{< relref "field-options/configure-specific-fields.md" >}})
## 5. Apply changes and save ## 5. Apply changes and save
@ -54,7 +58,5 @@ Our Grafana Fundamentals tutorial is a great place to start, or you can learn mo
- Learn more about [panel editor]({{< relref "panel-editor.md" >}}) options. - Learn more about [panel editor]({{< relref "panel-editor.md" >}}) options.
- Add more [queries]({{< relref "queries.md" >}}). - Add more [queries]({{< relref "queries.md" >}}).
- [Transform]({{< relref "transformations/_index.md" >}}) your data. - [Transform]({{< relref "transformations/_index.md" >}}) your data.
- [Configure]({{< relref "field-options/_index.md" >}}) how your results are displayed in the visualization. - Set up an [alert]({{< relref "../alerting/_index.md" >}}).
- If you made a graph panel, set up an [alert]({{< relref "../alerting/_index.md" >}}).
- Create [templates and variables]({{< relref "../variables/_index.md" >}}). - Create [templates and variables]({{< relref "../variables/_index.md" >}}).

View File

@ -1,7 +1,7 @@
+++ +++
title = "Legend options" title = "Legend options"
aliases = ["/docs/grafana/latest/panels/visualizations/panel-legend/"] aliases = ["/docs/grafana/latest/panels/visualizations/panel-legend/"]
weight = 500 weight = 950
+++ +++
# Legend options # Legend options

View File

@ -68,7 +68,6 @@ The section contains tabs where you control almost every aspect of how your data
Features in these tabs are documented in the following topics: Features in these tabs are documented in the following topics:
- [Add a panel]({{< relref "add-a-panel.md" >}}) describes basic panel settings. - [Add a panel]({{< relref "add-a-panel.md" >}}) describes basic panel settings.
- [Visualizations]({{< relref "visualizations/_index.md" >}}) display options vary widely. They are described in the individual visualization topic. - [Visualization]({{< relref "visualizations/_index.md" >}}) options vary widely. They are described in the individual visualization topic.
- [Field options and overrides]({{< relref "field-options/_index.md" >}}) allow you to control almost every aspect of your visualization, including units, value mappings, and [Thresholds]({{< relref "thresholds.md" >}}). - [Field options and overrides]({{< relref "field-options/_index.md" >}}) allow you to control almost every aspect of your visualization, including units, value mappings, and [Thresholds]({{< relref "thresholds.md" >}}).
- [Panel links]({{< relref "../linking/panel-links.md" >}}) and [Data links]({{< relref "../linking/data-links.md" >}}) help you connect your visualization to other resources. - [Panel links]({{< relref "../linking/panel-links.md" >}}) and [Data links]({{< relref "../linking/data-links.md" >}}) help you connect your visualization to other resources.

View File

@ -0,0 +1,38 @@
+++
title = "Panel options"
weight = 900
+++
# Panel options
Panel options are common to all panels. They are basic options to add information and clarity to your panels. Fields are described below.
While not required, we recommend that you add a helpful title and description to all panels.
![](/img/docs/panels/panel-options-8-0.png)
## Title
Text entered in this field is displayed at the top of your panel in the panel editor and in the dashboard. You can use [variables you have defined]({{< relref "../variables/_index.md" >}}) in either field, but not [global variables]({{< relref "../variables/variable-types/global-variables.md" >}}).
## Description
Text entered in this field is displayed in a tooltip in the upper left corner of the panel. Write a description of the panel and the data you are displaying. Pretend you are explaining it to a new user six months from now, when it is no longer fresh in your mind. Future editors (possibly yourself) will thank you.
You can use [variables you have defined]({{< relref "../variables/_index.md" >}}) in either field, but not [global variables]({{< relref "../variables/variable-types/global-variables.md" >}}).
## Transparent background
Toggle the transparent background option on your panel display.
## Panel links
{{< docs/shared "panels/panel-links-intro.md" >}}
For more information, refer to [Panel links]({{< relref "../linking/panel-links.md" >}}).
## Repeat options
{{< docs/shared "panels/repeat-panels-intro.md" >}}
For more information, refer to [Repeat panels or rows]({{< relref "./repeat-panels-or-rows.md" >}}).

View File

@ -1,12 +1,13 @@
+++ +++
title = "Repeat panels or rows" title = "Repeat panels or rows"
keywords = ["grafana", "templating", "documentation", "guide", "template", "variable", "repeat"] keywords = ["grafana", "templating", "documentation", "guide", "template", "variable", "repeat"]
aliases = ["/docs/grafana/latest/variables/repeat-panels-or-rows/"]
weight = 800 weight = 800
+++ +++
# Repeat panels or rows # Repeat panels or rows
Grafana lets you create dynamic dashboards using _template variables_. All variables in your queries expand to the current value of the variable before the query is sent to the database. Variables let you reuse a single dashboard for all your services. {{< docs/shared "panels/repeat-panels-intro.md" >}}
Template variables can be very useful to dynamically change your queries across a whole dashboard. If you want Template variables can be very useful to dynamically change your queries across a whole dashboard. If you want
Grafana to dynamically create new panels or rows based on what values you have selected, you can use the _Repeat_ feature. Grafana to dynamically create new panels or rows based on what values you have selected, you can use the _Repeat_ feature.

View File

@ -1,12 +1,10 @@
+++ +++
title = "Visualizations" title = "Visualization options"
weight = 300 weight = 300
+++ +++
# Visualizations # Visualization options
Grafana offers a variety of visualizations to suit different use cases. This section of the documentation lists the different visualizations available in Grafana and their unique display settings. Grafana offers a variety of visualizations to suit different use cases. This section of the documentation lists the different visualizations available in Grafana and their unique options.
The default options and their unique display options are described in the pages in this section.
You can add more panel types with [plugins]({{< relref "../../plugins/_index.md" >}}). You can add more panel types with [plugins]({{< relref "../../plugins/_index.md" >}}).

View File

@ -8,6 +8,8 @@ weight = 50
# Permissions # Permissions
> Refer to [Fine-grained access Control]({{< relref "../enterprise/access-control/_index.md" >}}) in Grafana Enterprise for managing access with fine-grained permissions.
What you can do in Grafana is defined by the _permissions_ associated with your user account. What you can do in Grafana is defined by the _permissions_ associated with your user account.
There are three types of permissions: There are three types of permissions:
@ -23,6 +25,8 @@ You can be granted permissions based on:
- (Grafana Enterprise) Data source permissions. For more information, refer to [Data source permissions]({{< relref "../enterprise/datasource_permissions.md" >}}) in [Grafana Enterprise]({{< relref "../enterprise" >}}). - (Grafana Enterprise) Data source permissions. For more information, refer to [Data source permissions]({{< relref "../enterprise/datasource_permissions.md" >}}) in [Grafana Enterprise]({{< relref "../enterprise" >}}).
- (Grafana Cloud) Grafana Cloud has additional roles. For more information, refer to [Grafana Cloud roles and permissions](/docs/grafana-cloud/cloud-portal/cloud-roles/). - (Grafana Cloud) Grafana Cloud has additional roles. For more information, refer to [Grafana Cloud roles and permissions](/docs/grafana-cloud/cloud-portal/cloud-roles/).
If you are running Grafana Enterprise, you can grant access by using fine-grained roles and permissions, refer to [Fine-grained access Control]({{< relref "../enterprise/access-control/_index.md" >}}) for more information.
## Grafana Server Admin role ## Grafana Server Admin role
Grafana server administrators have the **Grafana Admin** flag enabled on their account. They can access the **Server Admin** menu and perform the following tasks: Grafana server administrators have the **Grafana Admin** flag enabled on their account. They can access the **Server Admin** menu and perform the following tasks:

View File

@ -7,6 +7,8 @@ weight = 100
# Organization roles # Organization roles
> Refer to [Fine-grained access Control]({{< relref "../enterprise/access-control/_index.md" >}}) in Grafana Enterprise for managing Organization roles with fine-grained permissions.
Users can belong to one or more organizations. A user's organization membership is tied to a role that defines what the user is allowed to do in that organization. Grafana supports multiple _organizations_ in order to support a wide variety of deployment models, including using a single Grafana instance to provide service to multiple potentially untrusted organizations. Users can belong to one or more organizations. A user's organization membership is tied to a role that defines what the user is allowed to do in that organization. Grafana supports multiple _organizations_ in order to support a wide variety of deployment models, including using a single Grafana instance to provide service to multiple potentially untrusted organizations.
In most cases, Grafana is deployed with a single organization. In most cases, Grafana is deployed with a single organization.
@ -36,6 +38,8 @@ The table below compares what each role can do. Read the sections below for more
| Change team settings | x | | | | Change team settings | x | | |
| Configure app plugins | x | | | | Configure app plugins | x | | |
If you are running Grafana Enterprise, you can grant and revoke access by using fine-grained roles and permissions, refer to [Fine-grained access Control]({{< relref "../enterprise/access-control/_index.md" >}}) for more information.
## Organization admin role ## Organization admin role
Can do everything scoped to the organization. For example: Can do everything scoped to the organization. For example:

View File

@ -5,6 +5,8 @@ weight = 500
# Restricting access # Restricting access
> Refer to [Fine-grained access Control]({{< relref "../enterprise/access-control/_index.md" >}}) in Grafana Enterprise to understand how to use fine-grained permissions to restrict access.
The highest permission always wins so if you for example want to hide a folder or dashboard from others you need to remove the **Organization Role** based permission from the Access Control List (ACL). The highest permission always wins so if you for example want to hide a folder or dashboard from others you need to remove the **Organization Role** based permission from the Access Control List (ACL).
- You cannot override permissions for users with the Organization Admin role. Admins always have access to everything. - You cannot override permissions for users with the Organization Admin role. Admins always have access to everything.

View File

@ -8,6 +8,7 @@ weight = 10000
Here you can find detailed release notes that list everything that is included in every release as well as notices 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. about deprecations, breaking changes as well as changes that relate to plugin development.
- [Release notes for 8.0.0-beta2]({{< relref "release-notes-8-0-0-beta2" >}})
- [Release notes for 8.0.0-beta1]({{< relref "release-notes-8-0-0-beta1" >}}) - [Release notes for 8.0.0-beta1]({{< relref "release-notes-8-0-0-beta1" >}})
- [Release notes for 7.5.7]({{< relref "release-notes-7-5-7" >}}) - [Release notes for 7.5.7]({{< relref "release-notes-7-5-7" >}})
- [Release notes for 7.5.6]({{< relref "release-notes-7-5-6" >}}) - [Release notes for 7.5.6]({{< relref "release-notes-7-5-6" >}})

View File

@ -0,0 +1,5 @@
---
title: Panel links intro
---
Each panel can have its own set of links that are shown in the upper left corner of the panel. You can link to any available URL, including dashboards, panels, or external sites. You can even control the time range to ensure the user is zoomed in on the right data in Grafana.

View File

@ -0,0 +1,5 @@
---
title: Repeat panel intro
---
Grafana lets you create dynamic dashboards using _template variables_. All variables in your queries expand to the current value of the variable before the query is sent to the database. Variables let you reuse a single dashboard for all your services.

View File

@ -22,30 +22,57 @@ These features are included in the Grafana open source edition.
Library panels allow users to build panels that can be used in multiple dashboards. Any updates made to that shared panel will then automatically be applied to all the dashboards that have that panel. Library panels allow users to build panels that can be used in multiple dashboards. Any updates made to that shared panel will then automatically be applied to all the dashboards that have that panel.
### Timeline panel ### Real-time streaming
Shows discrete status or state transitions of something over time. For example daily uptime or multi-sensor and digital I/O status. Data sources can now send real-time updates to dashboards over a websocket connection. This can be used with the [MQTT data source](https://github.com/grafana/mqtt-datasource).
### Bar chart panel In addition to data source integration, events can be sent to dashboards by posting metrics to the new live endpoint: `/api/live/push endpoint`.
New visualization that allows categorical data display. Following the new panel architecture supports field config and overrides, common tooltip, and legend options. These metrics will be broadcast to all dashboards connected to that stream endpoint.
### Time series panel updates ### Bar chart visualization (beta)
The Bar chart panel is a new visualization that allows categorical data display.
### State timeline visualization (beta)
This new visualization is designed to display state changes and durations.
### Status grid visualization (beta)
This new visualization is designed to display periodic status history.
### Histogram visualization (beta)
This release introduces a new histogram panel visualization.
### Time series visualization updates
The Time series is out of beta! We are removing the `Beta` tag and graduating the Time series visualization to a stable state.
The Time series is out of beta! We are removing the `Beta`tag and graduating the Time series panel to a stable state.
- **Time series** is now the default visualization option, replacing the **Graph (old)**. - **Time series** is now the default visualization option, replacing the **Graph (old)**.
- The Time series panel now supports stacking. For more information, refer to [Graph stacked time series]({{< relref "../panels/visualizations/time-series/graph-time-series-stacking.md" >}}). - The Time series panel now supports stacking. For more information, refer to [Graph stacked time series]({{< relref "../panels/visualizations/time-series/graph-time-series-stacking.md" >}}).
- You can now add alerts in the Time series panel, just like the old Graph panel. - You can now add alerts in the Time series panel, just like the old Graph panel.
- We added support for a shared crosshair and a tooltip thats now smarter when it comes to data display in the tooltip. - We added support for a shared crosshair and a tooltip thats now smarter when it comes to data display in the tooltip.
- Various performance improvements. - Various performance improvements.
[Time series panel]({{< relref "../panels/visualizations/time-series/_index.md" >}}) topics have been updated as a result of these changes.
### Pie chart visualization updates
The Pie chart is out of beta! We are removing the `Beta` tag and graduating the Pie chart visualization to a stable state.
### Panel editor updates ### Panel editor updates
Lots of panel editor improvements, heavily informed by user research and community feedback.
- All options are now shown in a single pane. - All options are now shown in a single pane.
- You can now search panel options. - You can now search panel options.
- Value mapping has been completely redesigned. - Value mapping has been completely redesigned.
- New **Table view** option is always available. - New **Table view** option is always available.
The [Panels]({{< relref "../panels/_index.md" >}}) section has been updated to reflect these changes.
### Look and feel update ### Look and feel update
Grafana 8 comes with a refreshed look and feel, including themes changed to be more accessible. The improved Grafana UI brings a number of adjustments and tweaks that make the application even more fun to use. Enjoy the new home dashboard design! Grafana 8 comes with a refreshed look and feel, including themes changed to be more accessible. The improved Grafana UI brings a number of adjustments and tweaks that make the application even more fun to use. Enjoy the new home dashboard design!
@ -181,6 +208,11 @@ You can now configure generic OAuth with empty scopes. This allows OAuth Identit
You can now configure generic OAuth with strict parsing of the `role_attribute_path`. By default, if th `role_attribute_path` property does not return a role, then the user is assigned the `Viewer` role. You can disable the role assignment by setting `role_attribute_strict = true`. It denies user access if no role or an invalid role is returned. You can now configure generic OAuth with strict parsing of the `role_attribute_path`. By default, if th `role_attribute_path` property does not return a role, then the user is assigned the `Viewer` role. You can disable the role assignment by setting `role_attribute_strict = true`. It denies user access if no role or an invalid role is returned.
#### Singlestat panel deprecated
Support for Singlestat panel has been discontinued. When you upgrade to version 8.0, all existing Singlestat panels automatically becomes Stat panels.
Stat panel is available as plugin.
## Enterprise features ## Enterprise features
These features are included in the Grafana Enterprise edition. These features are included in the Grafana Enterprise edition.

13
go.mod
View File

@ -14,8 +14,8 @@ replace k8s.io/client-go => k8s.io/client-go v0.18.8
require ( require (
cloud.google.com/go/storage v1.14.0 cloud.google.com/go/storage v1.14.0
cuelang.org/go v0.3.2 cuelang.org/go v0.3.2
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.16.0 github.com/Azure/azure-sdk-for-go/sdk/azcore v0.16.1
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.8.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.9.1
github.com/BurntSushi/toml v0.3.1 github.com/BurntSushi/toml v0.3.1
github.com/Masterminds/semver v1.5.0 github.com/Masterminds/semver v1.5.0
github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f
@ -51,8 +51,8 @@ require (
github.com/gosimple/slug v1.9.0 github.com/gosimple/slug v1.9.0
github.com/grafana/grafana-aws-sdk v0.4.0 github.com/grafana/grafana-aws-sdk v0.4.0
github.com/grafana/grafana-live-sdk v0.0.6 github.com/grafana/grafana-live-sdk v0.0.6
github.com/grafana/grafana-plugin-sdk-go v0.99.0 github.com/grafana/grafana-plugin-sdk-go v0.100.0
github.com/grafana/loki v1.6.2-0.20210510132741-f408e05ad426 github.com/grafana/loki v1.6.2-0.20210520072447-15d417efe103
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
github.com/hashicorp/go-hclog v0.16.0 github.com/hashicorp/go-hclog v0.16.0
github.com/hashicorp/go-plugin v1.4.0 github.com/hashicorp/go-plugin v1.4.0
@ -62,6 +62,7 @@ require (
github.com/jmespath/go-jmespath v0.4.0 github.com/jmespath/go-jmespath v0.4.0
github.com/json-iterator/go v1.1.11 github.com/json-iterator/go v1.1.11
github.com/jung-kurt/gofpdf v1.16.2 github.com/jung-kurt/gofpdf v1.16.2
github.com/laher/mergefs v0.1.1
github.com/lib/pq v1.10.0 github.com/lib/pq v1.10.0
github.com/linkedin/goavro/v2 v2.10.0 github.com/linkedin/goavro/v2 v2.10.0
github.com/magefile/mage v1.11.0 github.com/magefile/mage v1.11.0
@ -93,10 +94,10 @@ require (
go.opentelemetry.io/collector v0.25.0 go.opentelemetry.io/collector v0.25.0
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a
golang.org/x/exp v0.0.0-20210220032938-85be41e4509f // indirect golang.org/x/exp v0.0.0-20210220032938-85be41e4509f // indirect
golang.org/x/net v0.0.0-20210510120150-4163338589ed golang.org/x/net v0.0.0-20210521195947-fe42d452be8f
golang.org/x/oauth2 v0.0.0-20210413134643-5e61552d6c78 golang.org/x/oauth2 v0.0.0-20210413134643-5e61552d6c78
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 // indirect golang.org/x/sys v0.0.0-20210521203332-0cec03c779c1 // indirect
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba
golang.org/x/tools v0.1.0 golang.org/x/tools v0.1.0
gonum.org/v1/gonum v0.9.1 gonum.org/v1/gonum v0.9.1

28
go.sum
View File

@ -78,10 +78,10 @@ github.com/Azure/azure-sdk-for-go v51.2.0+incompatible/go.mod h1:9XXNKU+eRnpl9mo
github.com/Azure/azure-sdk-for-go v52.5.0+incompatible h1:/NLBWHCnIHtZyLPc1P7WIqi4Te4CC23kIQyK3Ep/7lA= github.com/Azure/azure-sdk-for-go v52.5.0+incompatible h1:/NLBWHCnIHtZyLPc1P7WIqi4Te4CC23kIQyK3Ep/7lA=
github.com/Azure/azure-sdk-for-go v52.5.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v52.5.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.14.0/go.mod h1:pElNP+u99BvCZD+0jOlhI9OC/NB2IDTOTGZOZH0Qhq8= github.com/Azure/azure-sdk-for-go/sdk/azcore v0.14.0/go.mod h1:pElNP+u99BvCZD+0jOlhI9OC/NB2IDTOTGZOZH0Qhq8=
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.16.0 h1:ZsS7JltN+5D42mcU3Mb4lwVivlFL89v+FlXXMXE2YEM= github.com/Azure/azure-sdk-for-go/sdk/azcore v0.16.1 h1:yQw8Ah26gBP4dv66ZNjZpRBRV+gaHH/0TLn1taU4FZ4=
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.16.0/go.mod h1:MVdrcUC4Hup35qHym3VdzoW+NBgBxrta9Vei97jRtM8= github.com/Azure/azure-sdk-for-go/sdk/azcore v0.16.1/go.mod h1:MVdrcUC4Hup35qHym3VdzoW+NBgBxrta9Vei97jRtM8=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.8.0 h1:wb00szFWtKeIef2Q5X8gdd0mYp8oSHmJOYUh/QXD8sw= github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.9.1 h1:KchdKK3XlOjkzBROV+q3D+YgfRTvwoeBwbaoX4aVkjI=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.8.0/go.mod h1:acANgl9stsT5xflESXKjZx4rhZJSr0TGgTDYY0xJPIE= github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.9.1/go.mod h1:acANgl9stsT5xflESXKjZx4rhZJSr0TGgTDYY0xJPIE=
github.com/Azure/azure-sdk-for-go/sdk/internal v0.5.0/go.mod h1:k4KbFSunV/+0hOHL1vyFaPsiYQ1Vmvy1TBpmtvCDLZM= github.com/Azure/azure-sdk-for-go/sdk/internal v0.5.0/go.mod h1:k4KbFSunV/+0hOHL1vyFaPsiYQ1Vmvy1TBpmtvCDLZM=
github.com/Azure/azure-sdk-for-go/sdk/internal v0.5.1 h1:vx8McI56N5oLSQu8xa+xdiE0fjQq8W8Zt49vHP8Rygw= github.com/Azure/azure-sdk-for-go/sdk/internal v0.5.1 h1:vx8McI56N5oLSQu8xa+xdiE0fjQq8W8Zt49vHP8Rygw=
github.com/Azure/azure-sdk-for-go/sdk/internal v0.5.1/go.mod h1:k4KbFSunV/+0hOHL1vyFaPsiYQ1Vmvy1TBpmtvCDLZM= github.com/Azure/azure-sdk-for-go/sdk/internal v0.5.1/go.mod h1:k4KbFSunV/+0hOHL1vyFaPsiYQ1Vmvy1TBpmtvCDLZM=
@ -922,10 +922,10 @@ github.com/grafana/grafana-live-sdk v0.0.6 h1:P1QFn0ZradOJp3zVpfG0STZMP+pgZrW0e0
github.com/grafana/grafana-live-sdk v0.0.6/go.mod h1:f15hHmWyLdFjmuWLsjeKeZnq/HnNQ3QkoPcaEww45AY= github.com/grafana/grafana-live-sdk v0.0.6/go.mod h1:f15hHmWyLdFjmuWLsjeKeZnq/HnNQ3QkoPcaEww45AY=
github.com/grafana/grafana-plugin-sdk-go v0.79.0/go.mod h1:NvxLzGkVhnoBKwzkst6CFfpMFKwAdIUZ1q8ssuLeF60= github.com/grafana/grafana-plugin-sdk-go v0.79.0/go.mod h1:NvxLzGkVhnoBKwzkst6CFfpMFKwAdIUZ1q8ssuLeF60=
github.com/grafana/grafana-plugin-sdk-go v0.91.0/go.mod h1:Ot3k7nY7P6DXmUsDgKvNB7oG1v7PRyTdmnYVoS554bU= github.com/grafana/grafana-plugin-sdk-go v0.91.0/go.mod h1:Ot3k7nY7P6DXmUsDgKvNB7oG1v7PRyTdmnYVoS554bU=
github.com/grafana/grafana-plugin-sdk-go v0.99.0 h1:pEmoSSYw7VsF+rhRgG4z+azE3eLwznomxVg9Ezppqzo= github.com/grafana/grafana-plugin-sdk-go v0.100.0 h1:BryvIFdx/HrsKMt2hkxN7cJ0WrCgKpgjdJW8y8TSol0=
github.com/grafana/grafana-plugin-sdk-go v0.99.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk= github.com/grafana/grafana-plugin-sdk-go v0.100.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk=
github.com/grafana/loki v1.6.2-0.20210510132741-f408e05ad426 h1:fVUMdXAjiHsx71Twl/oie1OLDH+dxL7+mBdQK/H2Wgs= github.com/grafana/loki v1.6.2-0.20210520072447-15d417efe103 h1:qCmofFVwQR9QnsinstVqI1NPLMVl33jNCnOCXEAVn6E=
github.com/grafana/loki v1.6.2-0.20210510132741-f408e05ad426/go.mod h1:IfQ9BWq2sVAk3iKB4Pahz6QNTs5D4WpfJj/AY8xzmNw= github.com/grafana/loki v1.6.2-0.20210520072447-15d417efe103/go.mod h1:GHIsn+EohCChsdu5YouNZewqLeV9L2FNw4DEJU3P9qE=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
@ -1203,6 +1203,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g= github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
github.com/laher/mergefs v0.1.1 h1:nV2bTS57vrmbMxeR6uvJpI8LyGl3QHj4bLBZO3aUV58=
github.com/laher/mergefs v0.1.1/go.mod h1:FSY1hYy94on4Tz60waRMGdO1awwS23BacqJlqf9lJ9Q=
github.com/lann/builder v0.0.0-20150808151131-f22ce00fd939/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= github.com/lann/builder v0.0.0-20150808151131-f22ce00fd939/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
@ -1243,6 +1245,8 @@ github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ= github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
github.com/mattermost/xml-roundtrip-validator v0.0.0-20201213122252-bcd7e1b9601e h1:qqXczln0qwkVGcpQ+sQuPOVntt2FytYarXXxYSNJkgw= github.com/mattermost/xml-roundtrip-validator v0.0.0-20201213122252-bcd7e1b9601e h1:qqXczln0qwkVGcpQ+sQuPOVntt2FytYarXXxYSNJkgw=
github.com/mattermost/xml-roundtrip-validator v0.0.0-20201213122252-bcd7e1b9601e/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To= github.com/mattermost/xml-roundtrip-validator v0.0.0-20201213122252-bcd7e1b9601e/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To=
@ -2088,8 +2092,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210324051636-2c4c8ecb7826/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210324051636-2c4c8ecb7826/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20210510120150-4163338589ed h1:p9UgmWI9wKpfYmgaV/IZKGdXc5qEK45tDwwwDyjS26I= golang.org/x/net v0.0.0-20210521195947-fe42d452be8f h1:Si4U+UcgJzya9kpiEUJKQvjr512OLli+gL4poHrz93U=
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210521195947-fe42d452be8f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -2236,8 +2240,8 @@ golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210412220455-f1c623a9e750/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210412220455-f1c623a9e750/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 h1:hZR0X1kPW+nwyJ9xRxqZk1vx5RUObAPBdKVvXPDUH/E= golang.org/x/sys v0.0.0-20210521203332-0cec03c779c1 h1:lCnv+lfrU9FRPGf8NeRuWAAPjNnema5WtBinMgs1fD8=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210521203332-0cec03c779c1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@ -1,4 +1,4 @@
{ {
"stable": "7.5.7", "stable": "7.5.7",
"testing": "8.0.0-beta1" "testing": "8.0.0-beta2"
} }

View File

@ -103,7 +103,6 @@
"@types/file-saver": "2.0.1", "@types/file-saver": "2.0.1",
"@types/history": "^4.7.8", "@types/history": "^4.7.8",
"@types/hoist-non-react-statics": "3.3.1", "@types/hoist-non-react-statics": "3.3.1",
"@types/is-hotkey": "0.1.1",
"@types/jest": "26.0.15", "@types/jest": "26.0.15",
"@types/jquery": "3.3.38", "@types/jquery": "3.3.38",
"@types/jsurl": "^1.2.28", "@types/jsurl": "^1.2.28",
@ -256,8 +255,6 @@
"history": "4.10.1", "history": "4.10.1",
"hoist-non-react-statics": "3.3.2", "hoist-non-react-statics": "3.3.2",
"immer": "8.0.1", "immer": "8.0.1",
"immutable": "3.8.2",
"is-hotkey": "0.1.6",
"jquery": "3.5.1", "jquery": "3.5.1",
"json-source-map": "0.6.1", "json-source-map": "0.6.1",
"jsurl": "^0.1.5", "jsurl": "^0.1.5",
@ -281,7 +278,6 @@
"react-dom": "17.0.1", "react-dom": "17.0.1",
"react-grid-layout": "1.2.0", "react-grid-layout": "1.2.0",
"react-highlight-words": "0.17.0", "react-highlight-words": "0.17.0",
"react-inlinesvg": "2.3.0",
"react-loadable": "5.5.0", "react-loadable": "5.5.0",
"react-popper": "2.2.4", "react-popper": "2.2.4",
"react-redux": "7.2.0", "react-redux": "7.2.0",
@ -312,7 +308,8 @@
"whatwg-fetch": "3.1.0" "whatwg-fetch": "3.1.0"
}, },
"resolutions": { "resolutions": {
"caniuse-db": "1.0.30000772" "caniuse-db": "1.0.30000772",
"underscore": "1.12.1"
}, },
"workspaces": { "workspaces": {
"packages": [ "packages": [

View File

@ -45,6 +45,7 @@
"d3": "5.15.0", "d3": "5.15.0",
"hoist-non-react-statics": "3.3.2", "hoist-non-react-statics": "3.3.2",
"immutable": "3.8.2", "immutable": "3.8.2",
"is-hotkey": "0.1.6",
"jquery": "3.5.1", "jquery": "3.5.1",
"lodash": "4.17.21", "lodash": "4.17.21",
"moment": "2.29.1", "moment": "2.29.1",
@ -62,6 +63,7 @@
"react-dom": "17.0.1", "react-dom": "17.0.1",
"react-highlight-words": "0.16.0", "react-highlight-words": "0.16.0",
"react-hook-form": "7.5.3", "react-hook-form": "7.5.3",
"react-inlinesvg": "2.3.0",
"react-popper": "2.2.4", "react-popper": "2.2.4",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-select": "4.3.0", "react-select": "4.3.0",
@ -86,6 +88,7 @@
"@types/common-tags": "^1.8.0", "@types/common-tags": "^1.8.0",
"@types/d3": "5.7.2", "@types/d3": "5.7.2",
"@types/hoist-non-react-statics": "3.3.1", "@types/hoist-non-react-statics": "3.3.1",
"@types/is-hotkey": "0.1.1",
"@types/jest": "26.0.15", "@types/jest": "26.0.15",
"@types/jquery": "3.3.38", "@types/jquery": "3.3.38",
"@types/lodash": "4.14.123", "@types/lodash": "4.14.123",

View File

@ -2,10 +2,12 @@ import React, { FC, useCallback, useEffect, useRef } from 'react';
import { isNil } from 'lodash'; import { isNil } from 'lodash';
import classNames from 'classnames'; import classNames from 'classnames';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import Scrollbars from 'react-custom-scrollbars'; import Scrollbars, { positionValues } from 'react-custom-scrollbars';
import { useStyles2 } from '../../themes'; import { useStyles2 } from '../../themes';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
export type ScrollbarPosition = positionValues;
interface Props { interface Props {
className?: string; className?: string;
autoHide?: boolean; autoHide?: boolean;
@ -15,7 +17,7 @@ interface Props {
hideHorizontalTrack?: boolean; hideHorizontalTrack?: boolean;
hideVerticalTrack?: boolean; hideVerticalTrack?: boolean;
scrollTop?: number; scrollTop?: number;
setScrollTop?: (event: any) => void; setScrollTop?: (position: ScrollbarPosition) => void;
autoHeightMin?: number | string; autoHeightMin?: number | string;
updateAfterMountMs?: number; updateAfterMountMs?: number;
} }
@ -101,11 +103,15 @@ export const CustomScrollbar: FC<Props> = ({
return <div {...passedProps} className="scrollbar-view" />; return <div {...passedProps} className="scrollbar-view" />;
}, []); }, []);
const onScrollStop = useCallback(() => {
ref.current && setScrollTop && setScrollTop(ref.current.getValues());
}, [setScrollTop]);
return ( return (
<Scrollbars <Scrollbars
ref={ref} ref={ref}
className={classNames(styles.customScrollbar, className)} className={classNames(styles.customScrollbar, className)}
onScroll={setScrollTop} onScrollStop={onScrollStop}
autoHeight={true} autoHeight={true}
autoHide={autoHide} autoHide={autoHide}
autoHideTimeout={autoHideTimeout} autoHideTimeout={autoHideTimeout}

View File

@ -24,7 +24,7 @@ export function Form<T>({
maxWidth = 600, maxWidth = 600,
...htmlProps ...htmlProps
}: FormProps<T>) { }: FormProps<T>) {
const { handleSubmit, register, control, trigger, getValues, formState, watch, setValue } = useForm<T>({ const { handleSubmit, trigger, formState, ...rest } = useForm<T>({
mode: validateOn, mode: validateOn,
defaultValues, defaultValues,
}); });
@ -45,7 +45,7 @@ export function Form<T>({
onSubmit={handleSubmit(onSubmit)} onSubmit={handleSubmit(onSubmit)}
{...htmlProps} {...htmlProps}
> >
{children({ register, errors: formState.errors, control, getValues, formState, watch, setValue })} {children({ errors: formState.errors, formState, ...rest })}
</form> </form>
); );
} }

View File

@ -23,6 +23,10 @@ export type Props = {
inline?: boolean; inline?: boolean;
size?: number; size?: number;
}; };
/**
* @public
*/
export const Spinner: FC<Props> = (props: Props) => { export const Spinner: FC<Props> = (props: Props) => {
const { className, inline = false, iconClassName, style, size = 16 } = props; const { className, inline = false, iconClassName, style, size = 16 } = props;
const styles = getStyles(size, inline); const styles = getStyles(size, inline);

View File

@ -6,7 +6,7 @@ export { Tooltip, PopoverContent } from './Tooltip/Tooltip';
export { PopoverController } from './Tooltip/PopoverController'; export { PopoverController } from './Tooltip/PopoverController';
export { Popover } from './Tooltip/Popover'; export { Popover } from './Tooltip/Popover';
export { Portal } from './Portal/Portal'; export { Portal } from './Portal/Portal';
export { CustomScrollbar } from './CustomScrollbar/CustomScrollbar'; export { CustomScrollbar, ScrollbarPosition } from './CustomScrollbar/CustomScrollbar';
export { TabbedContainer, TabConfig } from './TabbedContainer/TabbedContainer'; export { TabbedContainer, TabConfig } from './TabbedContainer/TabbedContainer';
export { ClipboardButton } from './ClipboardButton/ClipboardButton'; export { ClipboardButton } from './ClipboardButton/ClipboardButton';

View File

@ -1,10 +1,7 @@
import { UseFormReturn, FieldValues, FieldErrors } from 'react-hook-form'; import { UseFormReturn, FieldValues, FieldErrors } from 'react-hook-form';
export { SubmitHandler as FormsOnSubmit, FieldErrors as FormFieldErrors } from 'react-hook-form'; export { SubmitHandler as FormsOnSubmit, FieldErrors as FormFieldErrors } from 'react-hook-form';
export type FormAPI<T> = Pick< export type FormAPI<T> = Omit<UseFormReturn<T>, 'trigger' | 'handleSubmit'> & {
UseFormReturn<T>,
'register' | 'control' | 'formState' | 'getValues' | 'watch' | 'setValue'
> & {
errors: FieldErrors<T>; errors: FieldErrors<T>;
}; };

View File

@ -1348,10 +1348,6 @@ func (m *mockLibraryPanelService) ConnectLibraryPanelsForDashboard(c *models.Req
return nil return nil
} }
func (m *mockLibraryPanelService) ImportDashboard(c *models.ReqContext, dashboard *simplejson.Json, importedID int64) error {
return nil
}
type mockLibraryElementService struct { type mockLibraryElementService struct {
} }

View File

@ -285,16 +285,18 @@ func getPanelSort(id string) int {
sort = 10 sort = 10
case "status-grid": case "status-grid":
sort = 11 sort = 11
case "graph": case "histogram":
sort = 12 sort = 12
case "text": case "graph":
sort = 13 sort = 13
case "alertlist": case "text":
sort = 14 sort = 14
case "dashlist": case "alertlist":
sort = 15 sort = 15
case "news": case "dashlist":
sort = 16 sort = 16
case "news":
sort = 17
} }
return sort return sort
} }

View File

@ -5,6 +5,7 @@ import (
"sort" "sort"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"time" "time"
) )
@ -15,6 +16,7 @@ type AccessToken struct {
type TokenCredential interface { type TokenCredential interface {
GetCacheKey() string GetCacheKey() string
Init() error
GetAccessToken(ctx context.Context, scopes []string) (*AccessToken, error) GetAccessToken(ctx context.Context, scopes []string) (*AccessToken, error)
} }
@ -31,6 +33,9 @@ type tokenCacheImpl struct {
} }
type credentialCacheEntry struct { type credentialCacheEntry struct {
credential TokenCredential credential TokenCredential
credInit uint32
credMutex sync.Mutex
cache sync.Map // of *scopesCacheEntry cache sync.Map // of *scopesCacheEntry
} }
@ -44,31 +49,67 @@ type scopesCacheEntry struct {
} }
func (c *tokenCacheImpl) GetAccessToken(ctx context.Context, credential TokenCredential, scopes []string) (string, error) { func (c *tokenCacheImpl) GetAccessToken(ctx context.Context, credential TokenCredential, scopes []string) (string, error) {
return c.getEntryFor(credential).getAccessToken(ctx, scopes)
}
func (c *tokenCacheImpl) getEntryFor(credential TokenCredential) *credentialCacheEntry {
var entry interface{} var entry interface{}
var ok bool var ok bool
credentialKey := credential.GetCacheKey() key := credential.GetCacheKey()
scopesKey := getKeyForScopes(scopes)
if entry, ok = c.cache.Load(credentialKey); !ok { if entry, ok = c.cache.Load(key); !ok {
entry, _ = c.cache.LoadOrStore(credentialKey, &credentialCacheEntry{ entry, _ = c.cache.LoadOrStore(key, &credentialCacheEntry{
credential: credential, credential: credential,
}) })
} }
credentialEntry := entry.(*credentialCacheEntry) return entry.(*credentialCacheEntry)
}
if entry, ok = credentialEntry.cache.Load(scopesKey); !ok { func (c *credentialCacheEntry) getAccessToken(ctx context.Context, scopes []string) (string, error) {
entry, _ = credentialEntry.cache.LoadOrStore(scopesKey, &scopesCacheEntry{ err := c.ensureInitialized()
credential: credentialEntry.credential, if err != nil {
return "", err
}
return c.getEntryFor(scopes).getAccessToken(ctx)
}
func (c *credentialCacheEntry) ensureInitialized() error {
if atomic.LoadUint32(&c.credInit) == 0 {
c.credMutex.Lock()
defer c.credMutex.Unlock()
if c.credInit == 0 {
// Initialize credential
err := c.credential.Init()
if err != nil {
return err
}
atomic.StoreUint32(&c.credInit, 1)
}
}
return nil
}
func (c *credentialCacheEntry) getEntryFor(scopes []string) *scopesCacheEntry {
var entry interface{}
var ok bool
key := getKeyForScopes(scopes)
if entry, ok = c.cache.Load(key); !ok {
entry, _ = c.cache.LoadOrStore(key, &scopesCacheEntry{
credential: c.credential,
scopes: scopes, scopes: scopes,
cond: sync.NewCond(&sync.Mutex{}), cond: sync.NewCond(&sync.Mutex{}),
}) })
} }
scopesEntry := entry.(*scopesCacheEntry) return entry.(*scopesCacheEntry)
return scopesEntry.getAccessToken(ctx)
} }
func (c *scopesCacheEntry) getAccessToken(ctx context.Context) (string, error) { func (c *scopesCacheEntry) getAccessToken(ctx context.Context) (string, error) {

View File

@ -14,7 +14,9 @@ import (
type fakeCredential struct { type fakeCredential struct {
key string key string
initCalledTimes int
calledTimes int calledTimes int
initFunc func() error
getAccessTokenFunc func(ctx context.Context, scopes []string) (*AccessToken, error) getAccessTokenFunc func(ctx context.Context, scopes []string) (*AccessToken, error)
} }
@ -22,6 +24,19 @@ func (c *fakeCredential) GetCacheKey() string {
return c.key return c.key
} }
func (c *fakeCredential) Reset() {
c.initCalledTimes = 0
c.calledTimes = 0
}
func (c *fakeCredential) Init() error {
c.initCalledTimes = c.initCalledTimes + 1
if c.initFunc != nil {
return c.initFunc()
}
return nil
}
func (c *fakeCredential) GetAccessToken(ctx context.Context, scopes []string) (*AccessToken, error) { func (c *fakeCredential) GetAccessToken(ctx context.Context, scopes []string) (*AccessToken, error) {
c.calledTimes = c.calledTimes + 1 c.calledTimes = c.calledTimes + 1
if c.getAccessTokenFunc != nil { if c.getAccessTokenFunc != nil {
@ -103,12 +118,168 @@ func TestConcurrentTokenCache_GetAccessToken(t *testing.T) {
}) })
} }
func TestCredentialCacheEntry_EnsureInitialized(t *testing.T) {
t.Run("when credential init returns error", func(t *testing.T) {
credential := &fakeCredential{
initFunc: func() error {
return errors.New("unable to initialize")
},
}
t.Run("should return error", func(t *testing.T) {
cacheEntry := &credentialCacheEntry{
credential: credential,
}
err := cacheEntry.ensureInitialized()
assert.Error(t, err)
})
t.Run("should call init again each time and return error", func(t *testing.T) {
credential.Reset()
cacheEntry := &credentialCacheEntry{
credential: credential,
}
var err error
err = cacheEntry.ensureInitialized()
assert.Error(t, err)
err = cacheEntry.ensureInitialized()
assert.Error(t, err)
err = cacheEntry.ensureInitialized()
assert.Error(t, err)
assert.Equal(t, 3, credential.initCalledTimes)
})
})
t.Run("when credential init returns error only once", func(t *testing.T) {
var times = 0
credential := &fakeCredential{
initFunc: func() error {
times = times + 1
if times == 1 {
return errors.New("unable to initialize")
}
return nil
},
}
t.Run("should call credential init again only while it returns error", func(t *testing.T) {
cacheEntry := &credentialCacheEntry{
credential: credential,
}
var err error
err = cacheEntry.ensureInitialized()
assert.Error(t, err)
err = cacheEntry.ensureInitialized()
assert.NoError(t, err)
err = cacheEntry.ensureInitialized()
assert.NoError(t, err)
assert.Equal(t, 2, credential.initCalledTimes)
})
})
t.Run("when credential init panics", func(t *testing.T) {
credential := &fakeCredential{
initFunc: func() error {
panic(errors.New("unable to initialize"))
},
}
t.Run("should call credential init again each time", func(t *testing.T) {
credential.Reset()
cacheEntry := &credentialCacheEntry{
credential: credential,
}
func() {
defer func() {
assert.NotNil(t, recover(), "credential expected to panic")
}()
_ = cacheEntry.ensureInitialized()
}()
func() {
defer func() {
assert.NotNil(t, recover(), "credential expected to panic")
}()
_ = cacheEntry.ensureInitialized()
}()
func() {
defer func() {
assert.NotNil(t, recover(), "credential expected to panic")
}()
_ = cacheEntry.ensureInitialized()
}()
assert.Equal(t, 3, credential.initCalledTimes)
})
})
t.Run("when credential init panics only once", func(t *testing.T) {
var times = 0
credential := &fakeCredential{
initFunc: func() error {
times = times + 1
if times == 1 {
panic(errors.New("unable to initialize"))
}
return nil
},
}
t.Run("should call credential init again only while it panics", func(t *testing.T) {
cacheEntry := &credentialCacheEntry{
credential: credential,
}
var err error
func() {
defer func() {
assert.NotNil(t, recover(), "credential expected to panic")
}()
_ = cacheEntry.ensureInitialized()
}()
func() {
defer func() {
assert.Nil(t, recover(), "credential not expected to panic")
}()
err = cacheEntry.ensureInitialized()
assert.NoError(t, err)
}()
func() {
defer func() {
assert.Nil(t, recover(), "credential not expected to panic")
}()
err = cacheEntry.ensureInitialized()
assert.NoError(t, err)
}()
assert.Equal(t, 2, credential.initCalledTimes)
})
})
}
func TestScopesCacheEntry_GetAccessToken(t *testing.T) { func TestScopesCacheEntry_GetAccessToken(t *testing.T) {
ctx := context.Background() ctx := context.Background()
scopes := []string{"Scope1"} scopes := []string{"Scope1"}
t.Run("when credential returns error", func(t *testing.T) { t.Run("when credential getAccessToken returns error", func(t *testing.T) {
credential := &fakeCredential{ credential := &fakeCredential{
getAccessTokenFunc: func(ctx context.Context, scopes []string) (*AccessToken, error) { getAccessTokenFunc: func(ctx context.Context, scopes []string) (*AccessToken, error) {
invalidToken := &AccessToken{Token: "invalid_token", ExpiresOn: timeNow().Add(time.Hour)} invalidToken := &AccessToken{Token: "invalid_token", ExpiresOn: timeNow().Add(time.Hour)}
@ -130,7 +301,7 @@ func TestScopesCacheEntry_GetAccessToken(t *testing.T) {
}) })
t.Run("should call credential again each time and return error", func(t *testing.T) { t.Run("should call credential again each time and return error", func(t *testing.T) {
credential.calledTimes = 0 credential.Reset()
cacheEntry := &scopesCacheEntry{ cacheEntry := &scopesCacheEntry{
credential: credential, credential: credential,
@ -152,7 +323,7 @@ func TestScopesCacheEntry_GetAccessToken(t *testing.T) {
}) })
}) })
t.Run("when credential returns error only once", func(t *testing.T) { t.Run("when credential getAccessToken returns error only once", func(t *testing.T) {
var times = 0 var times = 0
credential := &fakeCredential{ credential := &fakeCredential{
getAccessTokenFunc: func(ctx context.Context, scopes []string) (*AccessToken, error) { getAccessTokenFunc: func(ctx context.Context, scopes []string) (*AccessToken, error) {
@ -191,7 +362,7 @@ func TestScopesCacheEntry_GetAccessToken(t *testing.T) {
}) })
}) })
t.Run("when credential panics", func(t *testing.T) { t.Run("when credential getAccessToken panics", func(t *testing.T) {
credential := &fakeCredential{ credential := &fakeCredential{
getAccessTokenFunc: func(ctx context.Context, scopes []string) (*AccessToken, error) { getAccessTokenFunc: func(ctx context.Context, scopes []string) (*AccessToken, error) {
panic(errors.New("unable to get access token")) panic(errors.New("unable to get access token"))
@ -199,7 +370,7 @@ func TestScopesCacheEntry_GetAccessToken(t *testing.T) {
} }
t.Run("should call credential again each time", func(t *testing.T) { t.Run("should call credential again each time", func(t *testing.T) {
credential.calledTimes = 0 credential.Reset()
cacheEntry := &scopesCacheEntry{ cacheEntry := &scopesCacheEntry{
credential: credential, credential: credential,
@ -232,7 +403,7 @@ func TestScopesCacheEntry_GetAccessToken(t *testing.T) {
}) })
}) })
t.Run("when credential panics only once", func(t *testing.T) { t.Run("when credential getAccessToken panics only once", func(t *testing.T) {
var times = 0 var times = 0
credential := &fakeCredential{ credential := &fakeCredential{
getAccessTokenFunc: func(ctx context.Context, scopes []string) (*AccessToken, error) { getAccessTokenFunc: func(ctx context.Context, scopes []string) (*AccessToken, error) {

View File

@ -3,11 +3,8 @@ package pluginproxy
import ( import (
"context" "context"
"crypto/sha256" "crypto/sha256"
"errors"
"fmt" "fmt"
"strings" "strings"
"sync"
"sync/atomic"
"github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
@ -109,8 +106,7 @@ func (provider *azureAccessTokenProvider) resolveAuthorityHost(cloudName string)
type managedIdentityCredential struct { type managedIdentityCredential struct {
clientId string clientId string
credLock sync.Mutex credential azcore.TokenCredential
credValue atomic.Value // of azcore.TokenCredential
} }
func (c *managedIdentityCredential) GetCacheKey() string { func (c *managedIdentityCredential) GetCacheKey() string {
@ -121,39 +117,17 @@ func (c *managedIdentityCredential) GetCacheKey() string {
return fmt.Sprintf("azure|msi|%s", clientId) return fmt.Sprintf("azure|msi|%s", clientId)
} }
func (c *managedIdentityCredential) getCredential() (azcore.TokenCredential, error) { func (c *managedIdentityCredential) Init() error {
credential := c.credValue.Load() if credential, err := azidentity.NewManagedIdentityCredential(c.clientId, nil); err != nil {
return err
if credential == nil { } else {
c.credLock.Lock() c.credential = credential
defer c.credLock.Unlock() return nil
var err error
credential, err = azidentity.NewManagedIdentityCredential(c.clientId, nil)
if err != nil {
return nil, err
} }
c.credValue.Store(credential)
}
return credential.(azcore.TokenCredential), nil
} }
func (c *managedIdentityCredential) GetAccessToken(ctx context.Context, scopes []string) (*AccessToken, error) { func (c *managedIdentityCredential) GetAccessToken(ctx context.Context, scopes []string) (*AccessToken, error) {
credential, err := c.getCredential() accessToken, err := c.credential.GetToken(ctx, azcore.TokenRequestOptions{Scopes: scopes})
if err != nil {
return nil, err
}
// Implementation of ManagedIdentityCredential doesn't support scopes, converting to resource
if len(scopes) == 0 {
return nil, errors.New("scopes not provided")
}
resource := strings.TrimSuffix(scopes[0], "/.default")
scopes = []string{resource}
accessToken, err := credential.GetToken(ctx, azcore.TokenRequestOptions{Scopes: scopes})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -166,40 +140,25 @@ type clientSecretCredential struct {
tenantId string tenantId string
clientId string clientId string
clientSecret string clientSecret string
credLock sync.Mutex credential azcore.TokenCredential
credValue atomic.Value // of azcore.TokenCredential
} }
func (c *clientSecretCredential) GetCacheKey() string { func (c *clientSecretCredential) GetCacheKey() string {
return fmt.Sprintf("azure|clientsecret|%s|%s|%s|%s", c.authority, c.tenantId, c.clientId, hashSecret(c.clientSecret)) return fmt.Sprintf("azure|clientsecret|%s|%s|%s|%s", c.authority, c.tenantId, c.clientId, hashSecret(c.clientSecret))
} }
func (c *clientSecretCredential) getCredential() (azcore.TokenCredential, error) { func (c *clientSecretCredential) Init() error {
credential := c.credValue.Load() options := &azidentity.ClientSecretCredentialOptions{AuthorityHost: c.authority}
if credential, err := azidentity.NewClientSecretCredential(c.tenantId, c.clientId, c.clientSecret, options); err != nil {
if credential == nil { return err
c.credLock.Lock() } else {
defer c.credLock.Unlock() c.credential = credential
return nil
var err error
credential, err = azidentity.NewClientSecretCredential(c.tenantId, c.clientId, c.clientSecret, nil)
if err != nil {
return nil, err
} }
c.credValue.Store(credential)
}
return credential.(azcore.TokenCredential), nil
} }
func (c *clientSecretCredential) GetAccessToken(ctx context.Context, scopes []string) (*AccessToken, error) { func (c *clientSecretCredential) GetAccessToken(ctx context.Context, scopes []string) (*AccessToken, error) {
credential, err := c.getCredential() accessToken, err := c.credential.GetToken(ctx, azcore.TokenRequestOptions{Scopes: scopes})
if err != nil {
return nil, err
}
accessToken, err := credential.GetToken(ctx, azcore.TokenRequestOptions{Scopes: scopes})
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -220,13 +220,13 @@ func (hs *HTTPServer) ImportDashboard(c *models.ReqContext, apiCmd dtos.ImportDa
} }
} }
dashInfo, err := hs.PluginManager.ImportDashboard(apiCmd.PluginId, apiCmd.Path, c.OrgId, apiCmd.FolderId, dashInfo, dash, err := hs.PluginManager.ImportDashboard(apiCmd.PluginId, apiCmd.Path, c.OrgId, apiCmd.FolderId,
apiCmd.Dashboard, apiCmd.Overwrite, apiCmd.Inputs, c.SignedInUser, hs.DataService) apiCmd.Dashboard, apiCmd.Overwrite, apiCmd.Inputs, c.SignedInUser, hs.DataService)
if err != nil { if err != nil {
return hs.dashboardSaveErrorToApiResponse(err) return hs.dashboardSaveErrorToApiResponse(err)
} }
err = hs.LibraryPanelService.ImportDashboard(c, apiCmd.Dashboard, dashInfo.DashboardId) err = hs.LibraryPanelService.ConnectLibraryPanelsForDashboard(c, dash)
if err != nil { if err != nil {
return response.Error(500, "Error while connecting library panels", err) return response.Error(500, "Error while connecting library panels", err)
} }

View File

@ -1,92 +0,0 @@
package commands
import (
"errors"
"io/fs"
"os"
"sort"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
)
// MergeFS contains a slice of different filesystems that can be merged together
type MergeFS struct {
filesystems []fs.FS
}
// Merge filesystems
func Merge(filesystems ...fs.FS) fs.FS {
return MergeFS{filesystems: filesystems}
}
// Open opens the named file.
func (mfs MergeFS) Open(name string) (fs.File, error) {
for _, filesystem := range mfs.filesystems {
file, err := filesystem.Open(name)
if err == nil {
return file, nil
}
}
return nil, os.ErrNotExist
}
// ReadDir reads from the directory, and produces a DirEntry array of different
// directories.
//
// It iterates through all different filesystems that exist in the mfs MergeFS
// filesystem slice and it identifies overlapping directories that exist in different
// filesystems
func (mfs MergeFS) ReadDir(name string) ([]fs.DirEntry, error) {
dirsMap := make(map[string]fs.DirEntry)
for _, filesystem := range mfs.filesystems {
if fsys, ok := filesystem.(fs.ReadDirFS); ok {
dir, err := fsys.ReadDir(name)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
logger.Debugf("directory in filepath %s was not found in filesystem", name)
continue
}
return nil, err
}
for _, v := range dir {
if _, ok := dirsMap[v.Name()]; !ok {
dirsMap[v.Name()] = v
}
}
continue
}
file, err := filesystem.Open(name)
if err != nil {
logger.Debugf("filepath %s was not found in filesystem", name)
continue
}
dir, ok := file.(fs.ReadDirFile)
if !ok {
return nil, &fs.PathError{Op: "readdir", Path: name, Err: errors.New("not implemented")}
}
fsDirs, err := dir.ReadDir(-1)
if err != nil {
return nil, err
}
sort.Slice(fsDirs, func(i, j int) bool { return fsDirs[i].Name() < fsDirs[j].Name() })
for _, v := range fsDirs {
if _, ok := dirsMap[v.Name()]; !ok {
dirsMap[v.Name()] = v
}
}
if err := file.Close(); err != nil {
logger.Error("failed to close file", "err", err)
}
}
dirs := make([]fs.DirEntry, 0, len(dirsMap))
for _, value := range dirsMap {
dirs = append(dirs, value)
}
sort.Slice(dirs, func(i, j int) bool { return dirs[i].Name() < dirs[j].Name() })
return dirs, nil
}

View File

@ -1,68 +0,0 @@
package commands
import (
"io/fs"
"os"
"path/filepath"
"testing"
"testing/fstest"
"github.com/stretchr/testify/require"
)
func TestMergeFS(t *testing.T) {
var filePaths = []struct {
path string
dirArrayLength int
child string
}{
// MapFS takes in account the current directory in addition to all included directories and produces a "" dir
{"a", 1, "z"},
{"a/z", 1, "bar.cue"},
{"b", 1, "z"},
{"b/z", 1, "foo.cue"},
}
tempDir := os.DirFS(filepath.Join("testdata", "mergefs"))
a := fstest.MapFS{
"a": &fstest.MapFile{Mode: fs.ModeDir},
"a/z": &fstest.MapFile{Mode: fs.ModeDir},
"a/z/bar.cue": &fstest.MapFile{Data: []byte("bar")},
}
filesystem := Merge(tempDir, a)
t.Run("testing mergefs.ReadDir", func(t *testing.T) {
for _, fp := range filePaths {
t.Run("testing path: "+fp.path, func(t *testing.T) {
dirs, err := fs.ReadDir(filesystem, fp.path)
require.NoError(t, err)
require.Len(t, dirs, fp.dirArrayLength)
for i := 0; i < len(dirs); i++ {
require.Equal(t, dirs[i].Name(), fp.child)
}
})
}
})
t.Run("testing mergefs.Open", func(t *testing.T) {
data := make([]byte, 3)
file, err := filesystem.Open("a/z/bar.cue")
require.NoError(t, err)
_, err = file.Read(data)
require.NoError(t, err)
require.Equal(t, "bar", string(data))
file, err = filesystem.Open("b/z/foo.cue")
require.NoError(t, err)
_, err = file.Read(data)
require.NoError(t, err)
require.Equal(t, "foo", string(data))
err = file.Close()
require.NoError(t, err)
})
}

View File

@ -8,6 +8,7 @@ import (
"testing/fstest" "testing/fstest"
"github.com/grafana/grafana/pkg/schema/load" "github.com/grafana/grafana/pkg/schema/load"
"github.com/laher/mergefs"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -35,7 +36,7 @@ func TestValidateScuemataBasics(t *testing.T) {
filesystem := fstest.MapFS{ filesystem := fstest.MapFS{
"cue/data/gen.cue": &fstest.MapFile{Data: genCue}, "cue/data/gen.cue": &fstest.MapFile{Data: genCue},
} }
mergedFS := Merge(filesystem, defaultBaseLoadPaths.BaseCueFS) mergedFS := mergefs.Merge(filesystem, defaultBaseLoadPaths.BaseCueFS)
var baseLoadPaths = load.BaseLoadPaths{ var baseLoadPaths = load.BaseLoadPaths{
BaseCueFS: mergedFS, BaseCueFS: mergedFS,
@ -53,7 +54,7 @@ func TestValidateScuemataBasics(t *testing.T) {
filesystem := fstest.MapFS{ filesystem := fstest.MapFS{
"cue/data/gen.cue": &fstest.MapFile{Data: genCue}, "cue/data/gen.cue": &fstest.MapFile{Data: genCue},
} }
mergedFS := Merge(filesystem, defaultBaseLoadPaths.BaseCueFS) mergedFS := mergefs.Merge(filesystem, defaultBaseLoadPaths.BaseCueFS)
var baseLoadPaths = load.BaseLoadPaths{ var baseLoadPaths = load.BaseLoadPaths{
BaseCueFS: mergedFS, BaseCueFS: mergedFS,
@ -78,7 +79,7 @@ func TestValidateScuemataBasics(t *testing.T) {
"valid.json": &fstest.MapFile{Data: validPanel}, "valid.json": &fstest.MapFile{Data: validPanel},
"invalid.json": &fstest.MapFile{Data: invalidPanel}, "invalid.json": &fstest.MapFile{Data: invalidPanel},
} }
mergedFS := Merge(filesystem, defaultBaseLoadPaths.BaseCueFS) mergedFS := mergefs.Merge(filesystem, defaultBaseLoadPaths.BaseCueFS)
var baseLoadPaths = load.BaseLoadPaths{ var baseLoadPaths = load.BaseLoadPaths{
BaseCueFS: mergedFS, BaseCueFS: mergedFS,

View File

@ -185,6 +185,12 @@ var (
grafanaBuildVersion *prometheus.GaugeVec grafanaBuildVersion *prometheus.GaugeVec
grafanaPluginBuildInfoDesc *prometheus.GaugeVec grafanaPluginBuildInfoDesc *prometheus.GaugeVec
// StatsTotalLibraryPanels is a metric of total number of library panels stored in Grafana.
StatsTotalLibraryPanels prometheus.Gauge
// StatsTotalLibraryVariables is a metric of total number of library variables stored in Grafana.
StatsTotalLibraryVariables prometheus.Gauge
) )
func init() { func init() {
@ -547,6 +553,18 @@ func init() {
Help: "number of evaluation calls", Help: "number of evaluation calls",
Namespace: ExporterName, Namespace: ExporterName,
}) })
StatsTotalLibraryPanels = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "stat_totals_library_panels",
Help: "total amount of library panels in the database",
Namespace: ExporterName,
})
StatsTotalLibraryVariables = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "stat_totals_library_variables",
Help: "total amount of library variables in the database",
Namespace: ExporterName,
})
} }
// SetBuildInformation sets the build information for this binary // SetBuildInformation sets the build information for this binary
@ -640,6 +658,8 @@ func initMetricVars() {
StatsTotalDashboardVersions, StatsTotalDashboardVersions,
StatsTotalAnnotations, StatsTotalAnnotations,
MAccessEvaluationCount, MAccessEvaluationCount,
StatsTotalLibraryPanels,
StatsTotalLibraryVariables,
) )
} }

View File

@ -72,6 +72,8 @@ func (uss *UsageStatsService) GetUsageReport(ctx context.Context) (UsageReport,
metrics["stats.dashboard_versions.count"] = statsQuery.Result.DashboardVersions metrics["stats.dashboard_versions.count"] = statsQuery.Result.DashboardVersions
metrics["stats.annotations.count"] = statsQuery.Result.Annotations metrics["stats.annotations.count"] = statsQuery.Result.Annotations
metrics["stats.alert_rules.count"] = statsQuery.Result.AlertRules metrics["stats.alert_rules.count"] = statsQuery.Result.AlertRules
metrics["stats.library_panels.count"] = statsQuery.Result.LibraryPanels
metrics["stats.library_variables.count"] = statsQuery.Result.LibraryVariables
validLicCount := 0 validLicCount := 0
if uss.License.HasValidLicense() { if uss.License.HasValidLicense() {
validLicCount = 1 validLicCount = 1
@ -317,6 +319,8 @@ func (uss *UsageStatsService) updateTotalStats() {
metrics.StatsTotalDashboardVersions.Set(float64(statsQuery.Result.DashboardVersions)) metrics.StatsTotalDashboardVersions.Set(float64(statsQuery.Result.DashboardVersions))
metrics.StatsTotalAnnotations.Set(float64(statsQuery.Result.Annotations)) metrics.StatsTotalAnnotations.Set(float64(statsQuery.Result.Annotations))
metrics.StatsTotalAlertRules.Set(float64(statsQuery.Result.AlertRules)) metrics.StatsTotalAlertRules.Set(float64(statsQuery.Result.AlertRules))
metrics.StatsTotalLibraryPanels.Set(float64(statsQuery.Result.LibraryPanels))
metrics.StatsTotalLibraryVariables.Set(float64(statsQuery.Result.LibraryVariables))
dsStats := models.GetDataSourceStatsQuery{} dsStats := models.GetDataSourceStatsQuery{}
if err := uss.Bus.Dispatch(&dsStats); err != nil { if err := uss.Bus.Dispatch(&dsStats); err != nil {

View File

@ -63,6 +63,8 @@ func TestMetrics(t *testing.T) {
DashboardVersions: 16, DashboardVersions: 16,
Annotations: 17, Annotations: 17,
AlertRules: 18, AlertRules: 18,
LibraryPanels: 19,
LibraryVariables: 20,
} }
getSystemStatsQuery = query getSystemStatsQuery = query
return nil return nil
@ -313,6 +315,8 @@ func TestMetrics(t *testing.T) {
assert.Equal(t, 16, metrics.Get("stats.dashboard_versions.count").MustInt()) assert.Equal(t, 16, metrics.Get("stats.dashboard_versions.count").MustInt())
assert.Equal(t, 17, metrics.Get("stats.annotations.count").MustInt()) assert.Equal(t, 17, metrics.Get("stats.annotations.count").MustInt())
assert.Equal(t, 18, metrics.Get("stats.alert_rules.count").MustInt()) assert.Equal(t, 18, metrics.Get("stats.alert_rules.count").MustInt())
assert.Equal(t, 19, metrics.Get("stats.library_panels.count").MustInt())
assert.Equal(t, 20, metrics.Get("stats.library_variables.count").MustInt())
assert.Equal(t, 9, metrics.Get("stats.ds."+models.DS_ES+".count").MustInt()) assert.Equal(t, 9, metrics.Get("stats.ds."+models.DS_ES+".count").MustInt())
assert.Equal(t, 10, metrics.Get("stats.ds."+models.DS_PROMETHEUS+".count").MustInt()) assert.Equal(t, 10, metrics.Get("stats.ds."+models.DS_PROMETHEUS+".count").MustInt())

View File

@ -76,6 +76,7 @@ func (ds *DataSource) HTTPClientOptions() sdkhttpclient.Options {
opts := sdkhttpclient.Options{ opts := sdkhttpclient.Options{
Timeouts: &sdkhttpclient.TimeoutOptions{ Timeouts: &sdkhttpclient.TimeoutOptions{
Timeout: ds.getTimeout(), Timeout: ds.getTimeout(),
DialTimeout: time.Duration(setting.DataProxyDialTimeout) * time.Second,
KeepAlive: time.Duration(setting.DataProxyKeepAlive) * time.Second, KeepAlive: time.Duration(setting.DataProxyKeepAlive) * time.Second,
TLSHandshakeTimeout: time.Duration(setting.DataProxyTLSHandshakeTimeout) * time.Second, TLSHandshakeTimeout: time.Duration(setting.DataProxyTLSHandshakeTimeout) * time.Second,
ExpectContinueTimeout: time.Duration(setting.DataProxyExpectContinueTimeout) * time.Second, ExpectContinueTimeout: time.Duration(setting.DataProxyExpectContinueTimeout) * time.Second,

View File

@ -0,0 +1,13 @@
package models
// LibraryElementKind is used for the kind of library element
type LibraryElementKind int
const (
// PanelElement is used for library elements that are of the Panel kind
PanelElement LibraryElementKind = iota + 1
// VariableElement is used for library elements that are of the Variable kind
VariableElement
)
const LibraryElementConnectionTableName = "library_element_connection"

View File

@ -19,6 +19,8 @@ type SystemStats struct {
DashboardVersions int64 DashboardVersions int64
Annotations int64 Annotations int64
AlertRules int64 AlertRules int64
LibraryPanels int64
LibraryVariables int64
Admins int Admins int
Editors int Editors int

View File

@ -49,7 +49,7 @@ type Manager interface {
// ImportDashboard imports a dashboard. // ImportDashboard imports a dashboard.
ImportDashboard(pluginID, path string, orgID, folderID int64, dashboardModel *simplejson.Json, ImportDashboard(pluginID, path string, orgID, folderID int64, dashboardModel *simplejson.Json,
overwrite bool, inputs []ImportDashboardInput, user *models.SignedInUser, overwrite bool, inputs []ImportDashboardInput, user *models.SignedInUser,
requestHandler DataRequestHandler) (PluginDashboardInfoDTO, error) requestHandler DataRequestHandler) (PluginDashboardInfoDTO, *models.Dashboard, error)
// ScanningErrors returns plugin scanning errors encountered. // ScanningErrors returns plugin scanning errors encountered.
ScanningErrors() []PluginError ScanningErrors() []PluginError
// LoadPluginDashboard loads a plugin dashboard. // LoadPluginDashboard loads a plugin dashboard.

View File

@ -23,12 +23,12 @@ func (e DashboardInputMissingError) Error() string {
func (pm *PluginManager) ImportDashboard(pluginID, path string, orgID, folderID int64, dashboardModel *simplejson.Json, func (pm *PluginManager) ImportDashboard(pluginID, path string, orgID, folderID int64, dashboardModel *simplejson.Json,
overwrite bool, inputs []plugins.ImportDashboardInput, user *models.SignedInUser, overwrite bool, inputs []plugins.ImportDashboardInput, user *models.SignedInUser,
requestHandler plugins.DataRequestHandler) (plugins.PluginDashboardInfoDTO, error) { requestHandler plugins.DataRequestHandler) (plugins.PluginDashboardInfoDTO, *models.Dashboard, error) {
var dashboard *models.Dashboard var dashboard *models.Dashboard
if pluginID != "" { if pluginID != "" {
var err error var err error
if dashboard, err = pm.LoadPluginDashboard(pluginID, path); err != nil { if dashboard, err = pm.LoadPluginDashboard(pluginID, path); err != nil {
return plugins.PluginDashboardInfoDTO{}, err return plugins.PluginDashboardInfoDTO{}, &models.Dashboard{}, err
} }
} else { } else {
dashboard = models.NewDashboardFromJson(dashboardModel) dashboard = models.NewDashboardFromJson(dashboardModel)
@ -41,7 +41,7 @@ func (pm *PluginManager) ImportDashboard(pluginID, path string, orgID, folderID
generatedDash, err := evaluator.Eval() generatedDash, err := evaluator.Eval()
if err != nil { if err != nil {
return plugins.PluginDashboardInfoDTO{}, err return plugins.PluginDashboardInfoDTO{}, &models.Dashboard{}, err
} }
saveCmd := models.SaveDashboardCommand{ saveCmd := models.SaveDashboardCommand{
@ -62,7 +62,7 @@ func (pm *PluginManager) ImportDashboard(pluginID, path string, orgID, folderID
savedDash, err := dashboards.NewService(pm.SQLStore).ImportDashboard(dto) savedDash, err := dashboards.NewService(pm.SQLStore).ImportDashboard(dto)
if err != nil { if err != nil {
return plugins.PluginDashboardInfoDTO{}, err return plugins.PluginDashboardInfoDTO{}, &models.Dashboard{}, err
} }
return plugins.PluginDashboardInfoDTO{ return plugins.PluginDashboardInfoDTO{
@ -77,7 +77,7 @@ func (pm *PluginManager) ImportDashboard(pluginID, path string, orgID, folderID
Imported: true, Imported: true,
DashboardId: savedDash.Id, DashboardId: savedDash.Id,
Slug: savedDash.Slug, Slug: savedDash.Slug,
}, nil }, savedDash, nil
} }
type DashTemplateEvaluator struct { type DashTemplateEvaluator struct {

View File

@ -21,12 +21,13 @@ func TestDashboardImport(t *testing.T) {
mock := &dashboards.FakeDashboardService{} mock := &dashboards.FakeDashboardService{}
dashboards.MockDashboardService(mock) dashboards.MockDashboardService(mock)
info, err := pm.ImportDashboard("test-app", "dashboards/connections.json", 1, 0, nil, false, info, dash, err := pm.ImportDashboard("test-app", "dashboards/connections.json", 1, 0, nil, false,
[]plugins.ImportDashboardInput{ []plugins.ImportDashboardInput{
{Name: "*", Type: "datasource", Value: "graphite"}, {Name: "*", Type: "datasource", Value: "graphite"},
}, &models.SignedInUser{UserId: 1, OrgRole: models.ROLE_ADMIN}, nil) }, &models.SignedInUser{UserId: 1, OrgRole: models.ROLE_ADMIN}, nil)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, info) require.NotNil(t, info)
require.NotNil(t, dash)
resultStr, err := mock.SavedDashboards[0].Dashboard.Data.EncodePretty() resultStr, err := mock.SavedDashboards[0].Dashboard.Data.EncodePretty()
require.NoError(t, err) require.NoError(t, err)

View File

@ -60,30 +60,21 @@ func (e *BadRequestError) Error() string {
type ErrVersionUnsupported struct { type ErrVersionUnsupported struct {
PluginID string PluginID string
RequestedVersion string RequestedVersion string
RecommendedVersion string SystemInfo string
} }
func (e ErrVersionUnsupported) Error() string { func (e ErrVersionUnsupported) Error() string {
if len(e.RecommendedVersion) > 0 { return fmt.Sprintf("%s v%s is not supported on your system (%s)", e.PluginID, e.RequestedVersion, e.SystemInfo)
return fmt.Sprintf("%s v%s is not supported on your architecture and OS, latest suitable version is %s",
e.PluginID, e.RequestedVersion, e.RecommendedVersion)
}
return fmt.Sprintf("%s v%s is not supported on your architecture and OS", e.PluginID, e.RequestedVersion)
} }
type ErrVersionNotFound struct { type ErrVersionNotFound struct {
PluginID string PluginID string
RequestedVersion string RequestedVersion string
RecommendedVersion string SystemInfo string
} }
func (e ErrVersionNotFound) Error() string { func (e ErrVersionNotFound) Error() string {
if len(e.RecommendedVersion) > 0 { return fmt.Sprintf("%s v%s either does not exist or is not supported on your system (%s)", e.PluginID, e.RequestedVersion, e.SystemInfo)
return fmt.Sprintf("%s v%s is not supported on your architecture and OS, latest suitable version is %s",
e.PluginID, e.RequestedVersion, e.RecommendedVersion)
}
return fmt.Sprintf("could not find a version %s for %s. The latest suitable version is %s", e.RequestedVersion,
e.PluginID, e.RecommendedVersion)
} }
func New(skipTLSVerify bool, grafanaVersion string, logger plugins.PluginInstallerLogger) *Installer { func New(skipTLSVerify bool, grafanaVersion string, logger plugins.PluginInstallerLogger) *Installer {
@ -114,7 +105,7 @@ func (i *Installer) Install(ctx context.Context, pluginID, version, pluginsDir,
return err return err
} }
v, err := selectVersion(&plugin, version) v, err := i.selectVersion(&plugin, version)
if err != nil { if err != nil {
return err return err
} }
@ -429,7 +420,7 @@ func normalizeVersion(version string) string {
// selectVersion returns latest version if none is specified or the specified version. If the version string is not // selectVersion returns latest version if none is specified or the specified version. If the version string is not
// matched to existing version it errors out. It also errors out if version that is matched is not available for current // matched to existing version it errors out. It also errors out if version that is matched is not available for current
// os and platform. It expects plugin.Versions to be sorted so the newest version is first. // os and platform. It expects plugin.Versions to be sorted so the newest version is first.
func selectVersion(plugin *Plugin, version string) (*Version, error) { func (i *Installer) selectVersion(plugin *Plugin, version string) (*Version, error) {
var ver Version var ver Version
latestForArch := latestSupportedVersion(plugin) latestForArch := latestSupportedVersion(plugin)
@ -437,6 +428,7 @@ func selectVersion(plugin *Plugin, version string) (*Version, error) {
return nil, ErrVersionUnsupported{ return nil, ErrVersionUnsupported{
PluginID: plugin.ID, PluginID: plugin.ID,
RequestedVersion: version, RequestedVersion: version,
SystemInfo: i.fullSystemInfoString(),
} }
} }
@ -451,24 +443,32 @@ func selectVersion(plugin *Plugin, version string) (*Version, error) {
} }
if len(ver.Version) == 0 { if len(ver.Version) == 0 {
i.log.Debugf("Requested plugin version %s v%s not found but potential fallback version '%s' was found",
plugin.ID, version, latestForArch.Version)
return nil, ErrVersionNotFound{ return nil, ErrVersionNotFound{
PluginID: plugin.ID, PluginID: plugin.ID,
RequestedVersion: version, RequestedVersion: version,
RecommendedVersion: latestForArch.Version, SystemInfo: i.fullSystemInfoString(),
} }
} }
if !supportsCurrentArch(&ver) { if !supportsCurrentArch(&ver) {
i.log.Debugf("Requested plugin version %s v%s not found but potential fallback version '%s' was found",
plugin.ID, version, latestForArch.Version)
return nil, ErrVersionUnsupported{ return nil, ErrVersionUnsupported{
PluginID: plugin.ID, PluginID: plugin.ID,
RequestedVersion: version, RequestedVersion: version,
RecommendedVersion: latestForArch.Version, SystemInfo: i.fullSystemInfoString(),
} }
} }
return &ver, nil return &ver, nil
} }
func (i *Installer) fullSystemInfoString() string {
return fmt.Sprintf("Grafana v%s %s", i.grafanaVersion, osAndArchString())
}
func osAndArchString() string { func osAndArchString() string {
osString := strings.ToLower(runtime.GOOS) osString := strings.ToLower(runtime.GOOS)
arch := runtime.GOARCH arch := runtime.GOARCH

View File

@ -145,7 +145,7 @@ func (s *Service) autoUpdateAppDashboard(pluginDashInfo *plugins.PluginDashboard
s.logger.Info("Auto updating App dashboard", "dashboard", dash.Title, "newRev", s.logger.Info("Auto updating App dashboard", "dashboard", dash.Title, "newRev",
pluginDashInfo.Revision, "oldRev", pluginDashInfo.ImportedRevision) pluginDashInfo.Revision, "oldRev", pluginDashInfo.ImportedRevision)
user := &models.SignedInUser{UserId: 0, OrgRole: models.ROLE_ADMIN} user := &models.SignedInUser{UserId: 0, OrgRole: models.ROLE_ADMIN}
_, err = s.PluginManager.ImportDashboard(pluginDashInfo.PluginId, pluginDashInfo.Path, orgID, 0, dash.Data, true, _, _, err = s.PluginManager.ImportDashboard(pluginDashInfo.PluginId, pluginDashInfo.Path, orgID, 0, dash.Data, true,
nil, user, s.DataService) nil, user, s.DataService)
return err return err
} }

View File

@ -22,7 +22,7 @@ SELECT DISTINCT
, u1.email AS created_by_email , u1.email AS created_by_email
, u2.login AS updated_by_name , u2.login AS updated_by_name
, u2.email AS updated_by_email , u2.email AS updated_by_email
, (SELECT COUNT(connection_id) FROM ` + connectionTableName + ` WHERE element_id = le.id AND kind=1) AS connected_dashboards` , (SELECT COUNT(connection_id) FROM ` + models.LibraryElementConnectionTableName + ` WHERE element_id = le.id AND kind=1) AS connected_dashboards`
) )
func getFromLibraryElementDTOWithMeta(dialect migrator.Dialect) string { func getFromLibraryElementDTOWithMeta(dialect migrator.Dialect) string {
@ -41,9 +41,9 @@ func syncFieldsWithModel(libraryElement *LibraryElement) error {
return err return err
} }
if LibraryElementKind(libraryElement.Kind) == Panel { if models.LibraryElementKind(libraryElement.Kind) == models.PanelElement {
model["title"] = libraryElement.Name model["title"] = libraryElement.Name
} else if LibraryElementKind(libraryElement.Kind) == Variable { } else if models.LibraryElementKind(libraryElement.Kind) == models.VariableElement {
model["name"] = libraryElement.Name model["name"] = libraryElement.Name
} }
if model["type"] != nil { if model["type"] != nil {
@ -520,7 +520,7 @@ func (l *LibraryElementService) getConnections(c *models.ReqContext, uid string)
var libraryElementConnections []libraryElementConnectionWithMeta var libraryElementConnections []libraryElementConnectionWithMeta
builder := sqlstore.SQLBuilder{} builder := sqlstore.SQLBuilder{}
builder.Write("SELECT lec.*, u1.login AS created_by_name, u1.email AS created_by_email") builder.Write("SELECT lec.*, u1.login AS created_by_name, u1.email AS created_by_email")
builder.Write(" FROM " + connectionTableName + " AS lec") builder.Write(" FROM " + models.LibraryElementConnectionTableName + " AS lec")
builder.Write(" LEFT JOIN " + l.SQLStore.Dialect.Quote("user") + " AS u1 ON lec.created_by = u1.id") builder.Write(" LEFT JOIN " + l.SQLStore.Dialect.Quote("user") + " AS u1 ON lec.created_by = u1.id")
builder.Write(" INNER JOIN dashboard AS dashboard on lec.connection_id = dashboard.id") builder.Write(" INNER JOIN dashboard AS dashboard on lec.connection_id = dashboard.id")
builder.Write(` WHERE lec.element_id=?`, element.ID) builder.Write(` WHERE lec.element_id=?`, element.ID)
@ -562,7 +562,7 @@ func (l *LibraryElementService) getElementsForDashboardID(c *models.ReqContext,
", coalesce(dashboard.uid, '') AS folder_uid" + ", coalesce(dashboard.uid, '') AS folder_uid" +
getFromLibraryElementDTOWithMeta(l.SQLStore.Dialect) + getFromLibraryElementDTOWithMeta(l.SQLStore.Dialect) +
" LEFT JOIN dashboard AS dashboard ON dashboard.id = le.folder_id" + " LEFT JOIN dashboard AS dashboard ON dashboard.id = le.folder_id" +
" INNER JOIN " + connectionTableName + " AS lce ON lce.element_id = le.id AND lce.kind=1 AND lce.connection_id=?" " INNER JOIN " + models.LibraryElementConnectionTableName + " AS lce ON lce.element_id = le.id AND lce.kind=1 AND lce.connection_id=?"
sess := session.SQL(sql, dashboardID) sess := session.SQL(sql, dashboardID)
err := sess.Find(&libraryElements) err := sess.Find(&libraryElements)
if err != nil { if err != nil {
@ -610,7 +610,7 @@ func (l *LibraryElementService) getElementsForDashboardID(c *models.ReqContext,
// connectElementsToDashboardID adds connections for all elements Library Elements in a Dashboard. // connectElementsToDashboardID adds connections for all elements Library Elements in a Dashboard.
func (l *LibraryElementService) connectElementsToDashboardID(c *models.ReqContext, elementUIDs []string, dashboardID int64) error { func (l *LibraryElementService) connectElementsToDashboardID(c *models.ReqContext, elementUIDs []string, dashboardID int64) error {
err := l.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error { err := l.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
_, err := session.Exec("DELETE FROM "+connectionTableName+" WHERE kind=1 AND connection_id=?", dashboardID) _, err := session.Exec("DELETE FROM "+models.LibraryElementConnectionTableName+" WHERE kind=1 AND connection_id=?", dashboardID)
if err != nil { if err != nil {
return err return err
} }
@ -646,7 +646,7 @@ func (l *LibraryElementService) connectElementsToDashboardID(c *models.ReqContex
// disconnectElementsFromDashboardID deletes connections for all Library Elements in a Dashboard. // disconnectElementsFromDashboardID deletes connections for all Library Elements in a Dashboard.
func (l *LibraryElementService) disconnectElementsFromDashboardID(c *models.ReqContext, dashboardID int64) error { func (l *LibraryElementService) disconnectElementsFromDashboardID(c *models.ReqContext, dashboardID int64) error {
return l.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error { return l.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error {
_, err := session.Exec("DELETE FROM "+connectionTableName+" WHERE kind=1 AND connection_id=?", dashboardID) _, err := session.Exec("DELETE FROM "+models.LibraryElementConnectionTableName+" WHERE kind=1 AND connection_id=?", dashboardID)
if err != nil { if err != nil {
return err return err
} }
@ -676,7 +676,7 @@ func (l *LibraryElementService) deleteLibraryElementsInFolderUID(c *models.ReqCo
ConnectionID int64 `xorm:"connection_id"` ConnectionID int64 `xorm:"connection_id"`
} }
sql := "SELECT lec.connection_id FROM library_element AS le" sql := "SELECT lec.connection_id FROM library_element AS le"
sql += " INNER JOIN " + connectionTableName + " AS lec on le.id = lec.element_id" sql += " INNER JOIN " + models.LibraryElementConnectionTableName + " AS lec on le.id = lec.element_id"
sql += " WHERE le.folder_id=? AND le.org_id=?" sql += " WHERE le.folder_id=? AND le.org_id=?"
err = session.SQL(sql, folderID, c.SignedInUser.OrgId).Find(&connectionIDs) err = session.SQL(sql, folderID, c.SignedInUser.OrgId).Find(&connectionIDs)
if err != nil { if err != nil {
@ -694,7 +694,7 @@ func (l *LibraryElementService) deleteLibraryElementsInFolderUID(c *models.ReqCo
return err return err
} }
for _, elementID := range elementIDs { for _, elementID := range elementIDs {
_, err := session.Exec("DELETE FROM "+connectionTableName+" WHERE element_id=?", elementID.ID) _, err := session.Exec("DELETE FROM "+models.LibraryElementConnectionTableName+" WHERE element_id=?", elementID.ID)
if err != nil { if err != nil {
return err return err
} }

View File

@ -11,11 +11,11 @@ func isGeneralFolder(folderID int64) bool {
} }
func (l *LibraryElementService) requireSupportedElementKind(kindAsInt int64) error { func (l *LibraryElementService) requireSupportedElementKind(kindAsInt int64) error {
kind := LibraryElementKind(kindAsInt) kind := models.LibraryElementKind(kindAsInt)
switch kind { switch kind {
case Panel: case models.PanelElement:
return nil return nil
case Variable: case models.VariableElement:
return nil return nil
default: default:
return errLibraryElementUnSupportedElementKind return errLibraryElementUnSupportedElementKind

View File

@ -6,7 +6,6 @@ import (
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
@ -27,8 +26,6 @@ type LibraryElementService struct {
log log.Logger log log.Logger
} }
const connectionTableName = "library_element_connection"
func init() { func init() {
registry.RegisterService(&LibraryElementService{}) registry.RegisterService(&LibraryElementService{})
} }
@ -66,51 +63,3 @@ func (l *LibraryElementService) DisconnectElementsFromDashboard(c *models.ReqCon
func (l *LibraryElementService) DeleteLibraryElementsInFolder(c *models.ReqContext, folderUID string) error { func (l *LibraryElementService) DeleteLibraryElementsInFolder(c *models.ReqContext, folderUID string) error {
return l.deleteLibraryElementsInFolderUID(c, folderUID) return l.deleteLibraryElementsInFolderUID(c, folderUID)
} }
// AddMigration defines database migrations.
// If Panel Library is not enabled does nothing.
func (l *LibraryElementService) AddMigration(mg *migrator.Migrator) {
libraryElementsV1 := migrator.Table{
Name: "library_element",
Columns: []*migrator.Column{
{Name: "id", Type: migrator.DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
{Name: "org_id", Type: migrator.DB_BigInt, Nullable: false},
{Name: "folder_id", Type: migrator.DB_BigInt, Nullable: false},
{Name: "uid", Type: migrator.DB_NVarchar, Length: 40, Nullable: false},
{Name: "name", Type: migrator.DB_NVarchar, Length: 150, Nullable: false},
{Name: "kind", Type: migrator.DB_BigInt, Nullable: false},
{Name: "type", Type: migrator.DB_NVarchar, Length: 40, Nullable: false},
{Name: "description", Type: migrator.DB_NVarchar, Length: 255, Nullable: false},
{Name: "model", Type: migrator.DB_Text, Nullable: false},
{Name: "created", Type: migrator.DB_DateTime, Nullable: false},
{Name: "created_by", Type: migrator.DB_BigInt, Nullable: false},
{Name: "updated", Type: migrator.DB_DateTime, Nullable: false},
{Name: "updated_by", Type: migrator.DB_BigInt, Nullable: false},
{Name: "version", Type: migrator.DB_BigInt, Nullable: false},
},
Indices: []*migrator.Index{
{Cols: []string{"org_id", "folder_id", "name", "kind"}, Type: migrator.UniqueIndex},
},
}
mg.AddMigration("create library_element table v1", migrator.NewAddTableMigration(libraryElementsV1))
mg.AddMigration("add index library_element org_id-folder_id-name-kind", migrator.NewAddIndexMigration(libraryElementsV1, libraryElementsV1.Indices[0]))
libraryElementConnectionV1 := migrator.Table{
Name: connectionTableName,
Columns: []*migrator.Column{
{Name: "id", Type: migrator.DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
{Name: "element_id", Type: migrator.DB_BigInt, Nullable: false},
{Name: "kind", Type: migrator.DB_BigInt, Nullable: false},
{Name: "connection_id", Type: migrator.DB_BigInt, Nullable: false},
{Name: "created", Type: migrator.DB_DateTime, Nullable: false},
{Name: "created_by", Type: migrator.DB_BigInt, Nullable: false},
},
Indices: []*migrator.Index{
{Cols: []string{"element_id", "kind", "connection_id"}, Type: migrator.UniqueIndex},
},
}
mg.AddMigration("create "+connectionTableName+" table v1", migrator.NewAddTableMigration(libraryElementConnectionV1))
mg.AddMigration("add index "+connectionTableName+" element_id-kind-connection_id", migrator.NewAddIndexMigration(libraryElementConnectionV1, libraryElementConnectionV1.Indices[0]))
}

View File

@ -5,6 +5,8 @@ import (
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/models"
) )
func TestCreateLibraryElement(t *testing.T) { func TestCreateLibraryElement(t *testing.T) {
@ -24,7 +26,7 @@ func TestCreateLibraryElement(t *testing.T) {
FolderID: 1, FolderID: 1,
UID: sc.initialResult.Result.UID, UID: sc.initialResult.Result.UID,
Name: "Text - Library Panel", Name: "Text - Library Panel",
Kind: int64(Panel), Kind: int64(models.PanelElement),
Type: "text", Type: "text",
Description: "A description", Description: "A description",
Model: map[string]interface{}{ Model: map[string]interface{}{
@ -69,7 +71,7 @@ func TestCreateLibraryElement(t *testing.T) {
FolderID: 1, FolderID: 1,
UID: result.Result.UID, UID: result.Result.UID,
Name: "Library Panel Name", Name: "Library Panel Name",
Kind: int64(Panel), Kind: int64(models.PanelElement),
Type: "text", Type: "text",
Description: "A description", Description: "A description",
Model: map[string]interface{}{ Model: map[string]interface{}{

View File

@ -42,7 +42,7 @@ func TestGetAllLibraryElements(t *testing.T) {
err := sc.reqContext.Req.ParseForm() err := sc.reqContext.Req.ParseForm()
require.NoError(t, err) require.NoError(t, err)
sc.reqContext.Req.Form.Add("kind", strconv.FormatInt(int64(Panel), 10)) sc.reqContext.Req.Form.Add("kind", strconv.FormatInt(int64(models.PanelElement), 10))
resp = sc.service.getAllHandler(sc.reqContext) resp = sc.service.getAllHandler(sc.reqContext)
require.Equal(t, 200, resp.Status()) require.Equal(t, 200, resp.Status())
@ -62,7 +62,7 @@ func TestGetAllLibraryElements(t *testing.T) {
FolderID: 1, FolderID: 1,
UID: result.Result.Elements[0].UID, UID: result.Result.Elements[0].UID,
Name: "Text - Library Panel", Name: "Text - Library Panel",
Kind: int64(Panel), Kind: int64(models.PanelElement),
Type: "text", Type: "text",
Description: "A description", Description: "A description",
Model: map[string]interface{}{ Model: map[string]interface{}{
@ -107,7 +107,7 @@ func TestGetAllLibraryElements(t *testing.T) {
err := sc.reqContext.Req.ParseForm() err := sc.reqContext.Req.ParseForm()
require.NoError(t, err) require.NoError(t, err)
sc.reqContext.Req.Form.Add("kind", strconv.FormatInt(int64(Variable), 10)) sc.reqContext.Req.Form.Add("kind", strconv.FormatInt(int64(models.VariableElement), 10))
resp = sc.service.getAllHandler(sc.reqContext) resp = sc.service.getAllHandler(sc.reqContext)
require.Equal(t, 200, resp.Status()) require.Equal(t, 200, resp.Status())
@ -127,7 +127,7 @@ func TestGetAllLibraryElements(t *testing.T) {
FolderID: 1, FolderID: 1,
UID: result.Result.Elements[0].UID, UID: result.Result.Elements[0].UID,
Name: "query0", Name: "query0",
Kind: int64(Variable), Kind: int64(models.VariableElement),
Type: "query", Type: "query",
Description: "A description", Description: "A description",
Model: map[string]interface{}{ Model: map[string]interface{}{
@ -187,7 +187,7 @@ func TestGetAllLibraryElements(t *testing.T) {
FolderID: 1, FolderID: 1,
UID: result.Result.Elements[0].UID, UID: result.Result.Elements[0].UID,
Name: "Text - Library Panel", Name: "Text - Library Panel",
Kind: int64(Panel), Kind: int64(models.PanelElement),
Type: "text", Type: "text",
Description: "A description", Description: "A description",
Model: map[string]interface{}{ Model: map[string]interface{}{
@ -222,7 +222,7 @@ func TestGetAllLibraryElements(t *testing.T) {
FolderID: 1, FolderID: 1,
UID: result.Result.Elements[1].UID, UID: result.Result.Elements[1].UID,
Name: "Text - Library Panel2", Name: "Text - Library Panel2",
Kind: int64(Panel), Kind: int64(models.PanelElement),
Type: "text", Type: "text",
Description: "A description", Description: "A description",
Model: map[string]interface{}{ Model: map[string]interface{}{
@ -286,7 +286,7 @@ func TestGetAllLibraryElements(t *testing.T) {
FolderID: 1, FolderID: 1,
UID: result.Result.Elements[0].UID, UID: result.Result.Elements[0].UID,
Name: "Text - Library Panel2", Name: "Text - Library Panel2",
Kind: int64(Panel), Kind: int64(models.PanelElement),
Type: "text", Type: "text",
Description: "A description", Description: "A description",
Model: map[string]interface{}{ Model: map[string]interface{}{
@ -321,7 +321,7 @@ func TestGetAllLibraryElements(t *testing.T) {
FolderID: 1, FolderID: 1,
UID: result.Result.Elements[1].UID, UID: result.Result.Elements[1].UID,
Name: "Text - Library Panel", Name: "Text - Library Panel",
Kind: int64(Panel), Kind: int64(models.PanelElement),
Type: "text", Type: "text",
Description: "A description", Description: "A description",
Model: map[string]interface{}{ Model: map[string]interface{}{
@ -360,7 +360,7 @@ func TestGetAllLibraryElements(t *testing.T) {
scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and typeFilter is set to existing types, it should succeed and the result should be correct", scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and typeFilter is set to existing types, it should succeed and the result should be correct",
func(t *testing.T, sc scenarioContext) { func(t *testing.T, sc scenarioContext) {
command := getCreateCommandWithModel(sc.folder.Id, "Gauge - Library Panel", Panel, []byte(` command := getCreateCommandWithModel(sc.folder.Id, "Gauge - Library Panel", models.PanelElement, []byte(`
{ {
"datasource": "${DS_GDEV-TESTDATA}", "datasource": "${DS_GDEV-TESTDATA}",
"id": 1, "id": 1,
@ -372,7 +372,7 @@ func TestGetAllLibraryElements(t *testing.T) {
resp := sc.service.createHandler(sc.reqContext, command) resp := sc.service.createHandler(sc.reqContext, command)
require.Equal(t, 200, resp.Status()) require.Equal(t, 200, resp.Status())
command = getCreateCommandWithModel(sc.folder.Id, "BarGauge - Library Panel", Panel, []byte(` command = getCreateCommandWithModel(sc.folder.Id, "BarGauge - Library Panel", models.PanelElement, []byte(`
{ {
"datasource": "${DS_GDEV-TESTDATA}", "datasource": "${DS_GDEV-TESTDATA}",
"id": 1, "id": 1,
@ -405,7 +405,7 @@ func TestGetAllLibraryElements(t *testing.T) {
FolderID: 1, FolderID: 1,
UID: result.Result.Elements[0].UID, UID: result.Result.Elements[0].UID,
Name: "BarGauge - Library Panel", Name: "BarGauge - Library Panel",
Kind: int64(Panel), Kind: int64(models.PanelElement),
Type: "bargauge", Type: "bargauge",
Description: "BarGauge description", Description: "BarGauge description",
Model: map[string]interface{}{ Model: map[string]interface{}{
@ -440,7 +440,7 @@ func TestGetAllLibraryElements(t *testing.T) {
FolderID: 1, FolderID: 1,
UID: result.Result.Elements[1].UID, UID: result.Result.Elements[1].UID,
Name: "Gauge - Library Panel", Name: "Gauge - Library Panel",
Kind: int64(Panel), Kind: int64(models.PanelElement),
Type: "gauge", Type: "gauge",
Description: "Gauge description", Description: "Gauge description",
Model: map[string]interface{}{ Model: map[string]interface{}{
@ -479,7 +479,7 @@ func TestGetAllLibraryElements(t *testing.T) {
scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and typeFilter is set to a nonexistent type, it should succeed and the result should be correct", scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and typeFilter is set to a nonexistent type, it should succeed and the result should be correct",
func(t *testing.T, sc scenarioContext) { func(t *testing.T, sc scenarioContext) {
command := getCreateCommandWithModel(sc.folder.Id, "Gauge - Library Panel", Panel, []byte(` command := getCreateCommandWithModel(sc.folder.Id, "Gauge - Library Panel", models.PanelElement, []byte(`
{ {
"datasource": "${DS_GDEV-TESTDATA}", "datasource": "${DS_GDEV-TESTDATA}",
"id": 1, "id": 1,
@ -542,7 +542,7 @@ func TestGetAllLibraryElements(t *testing.T) {
FolderID: newFolder.Id, FolderID: newFolder.Id,
UID: result.Result.Elements[0].UID, UID: result.Result.Elements[0].UID,
Name: "Text - Library Panel2", Name: "Text - Library Panel2",
Kind: int64(Panel), Kind: int64(models.PanelElement),
Type: "text", Type: "text",
Description: "A description", Description: "A description",
Model: map[string]interface{}{ Model: map[string]interface{}{
@ -637,7 +637,7 @@ func TestGetAllLibraryElements(t *testing.T) {
FolderID: 1, FolderID: 1,
UID: result.Result.Elements[0].UID, UID: result.Result.Elements[0].UID,
Name: "Text - Library Panel", Name: "Text - Library Panel",
Kind: int64(Panel), Kind: int64(models.PanelElement),
Type: "text", Type: "text",
Description: "A description", Description: "A description",
Model: map[string]interface{}{ Model: map[string]interface{}{
@ -672,7 +672,7 @@ func TestGetAllLibraryElements(t *testing.T) {
FolderID: 1, FolderID: 1,
UID: result.Result.Elements[1].UID, UID: result.Result.Elements[1].UID,
Name: "Text - Library Panel2", Name: "Text - Library Panel2",
Kind: int64(Panel), Kind: int64(models.PanelElement),
Type: "text", Type: "text",
Description: "A description", Description: "A description",
Model: map[string]interface{}{ Model: map[string]interface{}{
@ -736,7 +736,7 @@ func TestGetAllLibraryElements(t *testing.T) {
FolderID: 1, FolderID: 1,
UID: result.Result.Elements[0].UID, UID: result.Result.Elements[0].UID,
Name: "Text - Library Panel2", Name: "Text - Library Panel2",
Kind: int64(Panel), Kind: int64(models.PanelElement),
Type: "text", Type: "text",
Description: "A description", Description: "A description",
Model: map[string]interface{}{ Model: map[string]interface{}{
@ -800,7 +800,7 @@ func TestGetAllLibraryElements(t *testing.T) {
FolderID: 1, FolderID: 1,
UID: result.Result.Elements[0].UID, UID: result.Result.Elements[0].UID,
Name: "Text - Library Panel", Name: "Text - Library Panel",
Kind: int64(Panel), Kind: int64(models.PanelElement),
Type: "text", Type: "text",
Description: "A description", Description: "A description",
Model: map[string]interface{}{ Model: map[string]interface{}{
@ -865,7 +865,7 @@ func TestGetAllLibraryElements(t *testing.T) {
FolderID: 1, FolderID: 1,
UID: result.Result.Elements[0].UID, UID: result.Result.Elements[0].UID,
Name: "Text - Library Panel2", Name: "Text - Library Panel2",
Kind: int64(Panel), Kind: int64(models.PanelElement),
Type: "text", Type: "text",
Description: "A description", Description: "A description",
Model: map[string]interface{}{ Model: map[string]interface{}{
@ -904,7 +904,7 @@ func TestGetAllLibraryElements(t *testing.T) {
scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and searchString exists in the description, it should succeed and the result should be correct", scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and searchString exists in the description, it should succeed and the result should be correct",
func(t *testing.T, sc scenarioContext) { func(t *testing.T, sc scenarioContext) {
command := getCreateCommandWithModel(sc.folder.Id, "Text - Library Panel2", Panel, []byte(` command := getCreateCommandWithModel(sc.folder.Id, "Text - Library Panel2", models.PanelElement, []byte(`
{ {
"datasource": "${DS_GDEV-TESTDATA}", "datasource": "${DS_GDEV-TESTDATA}",
"id": 1, "id": 1,
@ -939,7 +939,7 @@ func TestGetAllLibraryElements(t *testing.T) {
FolderID: 1, FolderID: 1,
UID: result.Result.Elements[0].UID, UID: result.Result.Elements[0].UID,
Name: "Text - Library Panel", Name: "Text - Library Panel",
Kind: int64(Panel), Kind: int64(models.PanelElement),
Type: "text", Type: "text",
Description: "A description", Description: "A description",
Model: map[string]interface{}{ Model: map[string]interface{}{
@ -978,7 +978,7 @@ func TestGetAllLibraryElements(t *testing.T) {
scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and searchString exists in both name and description, it should succeed and the result should be correct", scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and searchString exists in both name and description, it should succeed and the result should be correct",
func(t *testing.T, sc scenarioContext) { func(t *testing.T, sc scenarioContext) {
command := getCreateCommandWithModel(sc.folder.Id, "Some Other", Panel, []byte(` command := getCreateCommandWithModel(sc.folder.Id, "Some Other", models.PanelElement, []byte(`
{ {
"datasource": "${DS_GDEV-TESTDATA}", "datasource": "${DS_GDEV-TESTDATA}",
"id": 1, "id": 1,
@ -1011,7 +1011,7 @@ func TestGetAllLibraryElements(t *testing.T) {
FolderID: 1, FolderID: 1,
UID: result.Result.Elements[0].UID, UID: result.Result.Elements[0].UID,
Name: "Some Other", Name: "Some Other",
Kind: int64(Panel), Kind: int64(models.PanelElement),
Type: "text", Type: "text",
Description: "A Library Panel", Description: "A Library Panel",
Model: map[string]interface{}{ Model: map[string]interface{}{
@ -1046,7 +1046,7 @@ func TestGetAllLibraryElements(t *testing.T) {
FolderID: 1, FolderID: 1,
UID: result.Result.Elements[1].UID, UID: result.Result.Elements[1].UID,
Name: "Text - Library Panel", Name: "Text - Library Panel",
Kind: int64(Panel), Kind: int64(models.PanelElement),
Type: "text", Type: "text",
Description: "A description", Description: "A description",
Model: map[string]interface{}{ Model: map[string]interface{}{
@ -1112,7 +1112,7 @@ func TestGetAllLibraryElements(t *testing.T) {
FolderID: 1, FolderID: 1,
UID: result.Result.Elements[0].UID, UID: result.Result.Elements[0].UID,
Name: "Text - Library Panel2", Name: "Text - Library Panel2",
Kind: int64(Panel), Kind: int64(models.PanelElement),
Type: "text", Type: "text",
Description: "A description", Description: "A description",
Model: map[string]interface{}{ Model: map[string]interface{}{

View File

@ -35,7 +35,7 @@ func TestGetLibraryElement(t *testing.T) {
FolderID: 1, FolderID: 1,
UID: res.Result.UID, UID: res.Result.UID,
Name: "Text - Library Panel", Name: "Text - Library Panel",
Kind: int64(Panel), Kind: int64(models.PanelElement),
Type: "text", Type: "text",
Description: "A description", Description: "A description",
Model: map[string]interface{}{ Model: map[string]interface{}{
@ -130,7 +130,7 @@ func TestGetLibraryElement(t *testing.T) {
FolderID: 1, FolderID: 1,
UID: res.Result.UID, UID: res.Result.UID,
Name: "Text - Library Panel", Name: "Text - Library Panel",
Kind: int64(Panel), Kind: int64(models.PanelElement),
Type: "text", Type: "text",
Description: "A description", Description: "A description",
Model: map[string]interface{}{ Model: map[string]interface{}{

View File

@ -5,12 +5,14 @@ import (
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/models"
) )
func TestPatchLibraryElement(t *testing.T) { func TestPatchLibraryElement(t *testing.T) {
scenarioWithPanel(t, "When an admin tries to patch a library panel that does not exist, it should fail", scenarioWithPanel(t, "When an admin tries to patch a library panel that does not exist, it should fail",
func(t *testing.T, sc scenarioContext) { func(t *testing.T, sc scenarioContext) {
cmd := patchLibraryElementCommand{Kind: int64(Panel)} cmd := patchLibraryElementCommand{Kind: int64(models.PanelElement)}
sc.reqContext.ReplaceAllParams(map[string]string{":uid": "unknown"}) sc.reqContext.ReplaceAllParams(map[string]string{":uid": "unknown"})
resp := sc.service.patchHandler(sc.reqContext, cmd) resp := sc.service.patchHandler(sc.reqContext, cmd)
require.Equal(t, 404, resp.Status()) require.Equal(t, 404, resp.Status())
@ -31,7 +33,7 @@ func TestPatchLibraryElement(t *testing.T) {
"type": "graph" "type": "graph"
} }
`), `),
Kind: int64(Panel), Kind: int64(models.PanelElement),
Version: 1, Version: 1,
} }
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
@ -45,7 +47,7 @@ func TestPatchLibraryElement(t *testing.T) {
FolderID: newFolder.Id, FolderID: newFolder.Id,
UID: sc.initialResult.Result.UID, UID: sc.initialResult.Result.UID,
Name: "Panel - New name", Name: "Panel - New name",
Kind: int64(Panel), Kind: int64(models.PanelElement),
Type: "graph", Type: "graph",
Description: "An updated description", Description: "An updated description",
Model: map[string]interface{}{ Model: map[string]interface{}{
@ -83,7 +85,7 @@ func TestPatchLibraryElement(t *testing.T) {
newFolder := createFolderWithACL(t, sc.sqlStore, "NewFolder", sc.user, []folderACLItem{}) newFolder := createFolderWithACL(t, sc.sqlStore, "NewFolder", sc.user, []folderACLItem{})
cmd := patchLibraryElementCommand{ cmd := patchLibraryElementCommand{
FolderID: newFolder.Id, FolderID: newFolder.Id,
Kind: int64(Panel), Kind: int64(models.PanelElement),
Version: 1, Version: 1,
} }
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
@ -104,7 +106,7 @@ func TestPatchLibraryElement(t *testing.T) {
cmd := patchLibraryElementCommand{ cmd := patchLibraryElementCommand{
FolderID: -1, FolderID: -1,
Name: "New Name", Name: "New Name",
Kind: int64(Panel), Kind: int64(models.PanelElement),
Version: 1, Version: 1,
} }
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
@ -125,7 +127,7 @@ func TestPatchLibraryElement(t *testing.T) {
cmd := patchLibraryElementCommand{ cmd := patchLibraryElementCommand{
FolderID: -1, FolderID: -1,
Model: []byte(`{ "title": "New Model Title", "name": "New Model Name", "type":"graph", "description": "New description" }`), Model: []byte(`{ "title": "New Model Title", "name": "New Model Name", "type":"graph", "description": "New description" }`),
Kind: int64(Panel), Kind: int64(models.PanelElement),
Version: 1, Version: 1,
} }
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
@ -152,7 +154,7 @@ func TestPatchLibraryElement(t *testing.T) {
cmd := patchLibraryElementCommand{ cmd := patchLibraryElementCommand{
FolderID: -1, FolderID: -1,
Model: []byte(`{ "description": "New description" }`), Model: []byte(`{ "description": "New description" }`),
Kind: int64(Panel), Kind: int64(models.PanelElement),
Version: 1, Version: 1,
} }
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
@ -178,7 +180,7 @@ func TestPatchLibraryElement(t *testing.T) {
cmd := patchLibraryElementCommand{ cmd := patchLibraryElementCommand{
FolderID: -1, FolderID: -1,
Model: []byte(`{ "type": "graph" }`), Model: []byte(`{ "type": "graph" }`),
Kind: int64(Panel), Kind: int64(models.PanelElement),
Version: 1, Version: 1,
} }
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
@ -201,7 +203,7 @@ func TestPatchLibraryElement(t *testing.T) {
scenarioWithPanel(t, "When another admin tries to patch a library panel, it should change UpdatedBy successfully and return correct result", scenarioWithPanel(t, "When another admin tries to patch a library panel, it should change UpdatedBy successfully and return correct result",
func(t *testing.T, sc scenarioContext) { func(t *testing.T, sc scenarioContext) {
cmd := patchLibraryElementCommand{FolderID: -1, Version: 1, Kind: int64(Panel)} cmd := patchLibraryElementCommand{FolderID: -1, Version: 1, Kind: int64(models.PanelElement)}
sc.reqContext.UserId = 2 sc.reqContext.UserId = 2
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
resp := sc.service.patchHandler(sc.reqContext, cmd) resp := sc.service.patchHandler(sc.reqContext, cmd)
@ -223,7 +225,7 @@ func TestPatchLibraryElement(t *testing.T) {
cmd := patchLibraryElementCommand{ cmd := patchLibraryElementCommand{
Name: "Text - Library Panel", Name: "Text - Library Panel",
Version: 1, Version: 1,
Kind: int64(Panel), Kind: int64(models.PanelElement),
} }
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID}) sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID})
resp = sc.service.patchHandler(sc.reqContext, cmd) resp = sc.service.patchHandler(sc.reqContext, cmd)
@ -239,7 +241,7 @@ func TestPatchLibraryElement(t *testing.T) {
cmd := patchLibraryElementCommand{ cmd := patchLibraryElementCommand{
FolderID: 1, FolderID: 1,
Version: 1, Version: 1,
Kind: int64(Panel), Kind: int64(models.PanelElement),
} }
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID}) sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID})
resp = sc.service.patchHandler(sc.reqContext, cmd) resp = sc.service.patchHandler(sc.reqContext, cmd)
@ -251,7 +253,7 @@ func TestPatchLibraryElement(t *testing.T) {
cmd := patchLibraryElementCommand{ cmd := patchLibraryElementCommand{
FolderID: sc.folder.Id, FolderID: sc.folder.Id,
Version: 1, Version: 1,
Kind: int64(Panel), Kind: int64(models.PanelElement),
} }
sc.reqContext.OrgId = 2 sc.reqContext.OrgId = 2
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
@ -264,7 +266,7 @@ func TestPatchLibraryElement(t *testing.T) {
cmd := patchLibraryElementCommand{ cmd := patchLibraryElementCommand{
FolderID: sc.folder.Id, FolderID: sc.folder.Id,
Version: 1, Version: 1,
Kind: int64(Panel), Kind: int64(models.PanelElement),
} }
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
resp := sc.service.patchHandler(sc.reqContext, cmd) resp := sc.service.patchHandler(sc.reqContext, cmd)
@ -278,14 +280,14 @@ func TestPatchLibraryElement(t *testing.T) {
cmd := patchLibraryElementCommand{ cmd := patchLibraryElementCommand{
FolderID: sc.folder.Id, FolderID: sc.folder.Id,
Version: 1, Version: 1,
Kind: int64(Variable), Kind: int64(models.VariableElement),
} }
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID}) sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
resp := sc.service.patchHandler(sc.reqContext, cmd) resp := sc.service.patchHandler(sc.reqContext, cmd)
require.Equal(t, 200, resp.Status()) require.Equal(t, 200, resp.Status())
var result = validateAndUnMarshalResponse(t, resp) var result = validateAndUnMarshalResponse(t, resp)
sc.initialResult.Result.Type = "text" sc.initialResult.Result.Type = "text"
sc.initialResult.Result.Kind = int64(Panel) sc.initialResult.Result.Kind = int64(models.PanelElement)
sc.initialResult.Result.Description = "A description" sc.initialResult.Result.Description = "A description"
sc.initialResult.Result.Model = map[string]interface{}{ sc.initialResult.Result.Model = map[string]interface{}{
"datasource": "${DS_GDEV-TESTDATA}", "datasource": "${DS_GDEV-TESTDATA}",

View File

@ -84,7 +84,7 @@ func TestLibraryElementPermissions(t *testing.T) {
toFolder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, testCase.items) toFolder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, testCase.items)
sc.reqContext.SignedInUser.OrgRole = testCase.role sc.reqContext.SignedInUser.OrgRole = testCase.role
cmd := patchLibraryElementCommand{FolderID: toFolder.Id, Version: 1, Kind: int64(Panel)} cmd := patchLibraryElementCommand{FolderID: toFolder.Id, Version: 1, Kind: int64(models.PanelElement)}
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID}) sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID})
resp = sc.service.patchHandler(sc.reqContext, cmd) resp = sc.service.patchHandler(sc.reqContext, cmd)
require.Equal(t, testCase.status, resp.Status()) require.Equal(t, testCase.status, resp.Status())
@ -99,7 +99,7 @@ func TestLibraryElementPermissions(t *testing.T) {
toFolder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, everyonePermissions) toFolder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, everyonePermissions)
sc.reqContext.SignedInUser.OrgRole = testCase.role sc.reqContext.SignedInUser.OrgRole = testCase.role
cmd := patchLibraryElementCommand{FolderID: toFolder.Id, Version: 1, Kind: int64(Panel)} cmd := patchLibraryElementCommand{FolderID: toFolder.Id, Version: 1, Kind: int64(models.PanelElement)}
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID}) sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID})
resp = sc.service.patchHandler(sc.reqContext, cmd) resp = sc.service.patchHandler(sc.reqContext, cmd)
require.Equal(t, testCase.status, resp.Status()) require.Equal(t, testCase.status, resp.Status())
@ -146,7 +146,7 @@ func TestLibraryElementPermissions(t *testing.T) {
result := validateAndUnMarshalResponse(t, resp) result := validateAndUnMarshalResponse(t, resp)
sc.reqContext.SignedInUser.OrgRole = testCase.role sc.reqContext.SignedInUser.OrgRole = testCase.role
cmd := patchLibraryElementCommand{FolderID: 0, Version: 1, Kind: int64(Panel)} cmd := patchLibraryElementCommand{FolderID: 0, Version: 1, Kind: int64(models.PanelElement)}
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID}) sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID})
resp = sc.service.patchHandler(sc.reqContext, cmd) resp = sc.service.patchHandler(sc.reqContext, cmd)
require.Equal(t, testCase.status, resp.Status()) require.Equal(t, testCase.status, resp.Status())
@ -160,7 +160,7 @@ func TestLibraryElementPermissions(t *testing.T) {
result := validateAndUnMarshalResponse(t, resp) result := validateAndUnMarshalResponse(t, resp)
sc.reqContext.SignedInUser.OrgRole = testCase.role sc.reqContext.SignedInUser.OrgRole = testCase.role
cmd := patchLibraryElementCommand{FolderID: folder.Id, Version: 1, Kind: int64(Panel)} cmd := patchLibraryElementCommand{FolderID: folder.Id, Version: 1, Kind: int64(models.PanelElement)}
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID}) sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID})
resp = sc.service.patchHandler(sc.reqContext, cmd) resp = sc.service.patchHandler(sc.reqContext, cmd)
require.Equal(t, testCase.status, resp.Status()) require.Equal(t, testCase.status, resp.Status())
@ -205,7 +205,7 @@ func TestLibraryElementPermissions(t *testing.T) {
result := validateAndUnMarshalResponse(t, resp) result := validateAndUnMarshalResponse(t, resp)
sc.reqContext.SignedInUser.OrgRole = testCase.role sc.reqContext.SignedInUser.OrgRole = testCase.role
cmd := patchLibraryElementCommand{FolderID: -100, Version: 1, Kind: int64(Panel)} cmd := patchLibraryElementCommand{FolderID: -100, Version: 1, Kind: int64(models.PanelElement)}
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID}) sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID})
resp = sc.service.patchHandler(sc.reqContext, cmd) resp = sc.service.patchHandler(sc.reqContext, cmd)
require.Equal(t, 404, resp.Status()) require.Equal(t, 404, resp.Status())

View File

@ -126,7 +126,7 @@ type libraryElementsSearchResult struct {
} }
func getCreatePanelCommand(folderID int64, name string) CreateLibraryElementCommand { func getCreatePanelCommand(folderID int64, name string) CreateLibraryElementCommand {
command := getCreateCommandWithModel(folderID, name, Panel, []byte(` command := getCreateCommandWithModel(folderID, name, models.PanelElement, []byte(`
{ {
"datasource": "${DS_GDEV-TESTDATA}", "datasource": "${DS_GDEV-TESTDATA}",
"id": 1, "id": 1,
@ -140,7 +140,7 @@ func getCreatePanelCommand(folderID int64, name string) CreateLibraryElementComm
} }
func getCreateVariableCommand(folderID int64, name string) CreateLibraryElementCommand { func getCreateVariableCommand(folderID int64, name string) CreateLibraryElementCommand {
command := getCreateCommandWithModel(folderID, name, Variable, []byte(` command := getCreateCommandWithModel(folderID, name, models.VariableElement, []byte(`
{ {
"datasource": "${DS_GDEV-TESTDATA}", "datasource": "${DS_GDEV-TESTDATA}",
"name": "query0", "name": "query0",
@ -152,7 +152,7 @@ func getCreateVariableCommand(folderID int64, name string) CreateLibraryElementC
return command return command
} }
func getCreateCommandWithModel(folderID int64, name string, kind LibraryElementKind, model []byte) CreateLibraryElementCommand { func getCreateCommandWithModel(folderID int64, name string, kind models.LibraryElementKind, model []byte) CreateLibraryElementCommand {
command := CreateLibraryElementCommand{ command := CreateLibraryElementCommand{
FolderID: folderID, FolderID: folderID,
Name: name, Name: name,

View File

@ -6,13 +6,6 @@ import (
"time" "time"
) )
type LibraryElementKind int
const (
Panel LibraryElementKind = iota + 1
Variable
)
type LibraryConnectionKind int type LibraryConnectionKind int
const ( const (

View File

@ -5,6 +5,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore"
) )
@ -38,7 +39,7 @@ func writePerPageSQL(query searchLibraryElementsQuery, sqlStore *sqlstore.SQLSto
} }
func writeKindSQL(query searchLibraryElementsQuery, builder *sqlstore.SQLBuilder) { func writeKindSQL(query searchLibraryElementsQuery, builder *sqlstore.SQLBuilder) {
if LibraryElementKind(query.kind) == Panel || LibraryElementKind(query.kind) == Variable { if models.LibraryElementKind(query.kind) == models.PanelElement || models.LibraryElementKind(query.kind) == models.VariableElement {
builder.Write(" AND le.kind = ?", query.kind) builder.Write(" AND le.kind = ?", query.kind)
} }
} }

View File

@ -18,7 +18,6 @@ type Service interface {
LoadLibraryPanelsForDashboard(c *models.ReqContext, dash *models.Dashboard) error LoadLibraryPanelsForDashboard(c *models.ReqContext, dash *models.Dashboard) error
CleanLibraryPanelsForDashboard(dash *models.Dashboard) error CleanLibraryPanelsForDashboard(dash *models.Dashboard) error
ConnectLibraryPanelsForDashboard(c *models.ReqContext, dash *models.Dashboard) error ConnectLibraryPanelsForDashboard(c *models.ReqContext, dash *models.Dashboard) error
ImportDashboard(c *models.ReqContext, dashboard *simplejson.Json, importedID int64) error
} }
// LibraryPanelService is the service for the Panel Library feature. // LibraryPanelService is the service for the Panel Library feature.
@ -76,7 +75,7 @@ func (lps *LibraryPanelService) LoadLibraryPanelsForDashboard(c *models.ReqConte
continue continue
} }
if libraryelements.LibraryElementKind(elementInDB.Kind) != libraryelements.Panel { if models.LibraryElementKind(elementInDB.Kind) != models.PanelElement {
continue continue
} }
@ -193,11 +192,3 @@ func (lps *LibraryPanelService) ConnectLibraryPanelsForDashboard(c *models.ReqCo
return lps.LibraryElementService.ConnectElementsToDashboard(c, elementUIDs, dash.Id) return lps.LibraryElementService.ConnectElementsToDashboard(c, elementUIDs, dash.Id)
} }
// ImportDashboard loops through all panels in dashboard JSON and connects any library panels to the dashboard.
func (lps *LibraryPanelService) ImportDashboard(c *models.ReqContext, dashboard *simplejson.Json, importedID int64) error {
dash := models.NewDashboardFromJson(dashboard)
dash.Id = importedID
return lps.ConnectLibraryPanelsForDashboard(c, dash)
}

View File

@ -493,7 +493,7 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) {
"description": "Unused description" "description": "Unused description"
} }
`), `),
Kind: int64(libraryelements.Panel), Kind: int64(models.PanelElement),
}) })
require.NoError(t, err) require.NoError(t, err)
dashJSON := map[string]interface{}{ dashJSON := map[string]interface{}{
@ -572,106 +572,6 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) {
}) })
} }
func TestImportDashboard(t *testing.T) {
scenarioWithLibraryPanel(t, "When an admin tries to import a dashboard with a library panel, it should connect the two",
func(t *testing.T, sc scenarioContext) {
importedJSON := map[string]interface{}{
"panels": []interface{}{},
}
importedDashboard := models.Dashboard{
Title: "Dummy dash that simulates an imported dash",
Data: simplejson.NewFromAny(importedJSON),
}
importedDashInDB := createDashboard(t, sc.sqlStore, sc.user, &importedDashboard, sc.folder.Id)
elements, err := sc.elementService.GetElementsForDashboard(sc.reqContext, importedDashInDB.Id)
require.NoError(t, err)
require.Len(t, elements, 0)
dashJSON := map[string]interface{}{
"title": "Testing ImportDashboard",
"panels": []interface{}{
map[string]interface{}{
"id": int64(1),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 0,
},
},
map[string]interface{}{
"id": int64(2),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 6,
"y": 0,
},
"datasource": "${DS_GDEV-TESTDATA}",
"libraryPanel": map[string]interface{}{
"uid": sc.initialResult.Result.UID,
"name": sc.initialResult.Result.Name,
},
"title": "Text - Library Panel",
"type": "text",
},
},
}
dash := simplejson.NewFromAny(dashJSON)
err = sc.service.ImportDashboard(sc.reqContext, dash, importedDashInDB.Id)
require.NoError(t, err)
elements, err = sc.elementService.GetElementsForDashboard(sc.reqContext, importedDashInDB.Id)
require.NoError(t, err)
require.Len(t, elements, 1)
require.Equal(t, sc.initialResult.Result.UID, elements[sc.initialResult.Result.UID].UID)
})
scenarioWithLibraryPanel(t, "When an admin tries to import a dashboard with a library panel without uid, it should fail",
func(t *testing.T, sc scenarioContext) {
importedJSON := map[string]interface{}{
"panels": []interface{}{},
}
importedDashboard := models.Dashboard{
Title: "Dummy dash that simulates an imported dash",
Data: simplejson.NewFromAny(importedJSON),
}
importedDashInDB := createDashboard(t, sc.sqlStore, sc.user, &importedDashboard, sc.folder.Id)
dashJSON := map[string]interface{}{
"panels": []interface{}{
map[string]interface{}{
"id": int64(1),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 0,
"y": 0,
},
},
map[string]interface{}{
"id": int64(2),
"gridPos": map[string]interface{}{
"h": 6,
"w": 6,
"x": 6,
"y": 0,
},
"datasource": "${DS_GDEV-TESTDATA}",
"libraryPanel": map[string]interface{}{
"name": sc.initialResult.Result.Name,
},
"title": "Text - Library Panel",
"type": "text",
},
},
}
dash := simplejson.NewFromAny(dashJSON)
err := sc.service.ImportDashboard(sc.reqContext, dash, importedDashInDB.Id)
require.EqualError(t, err, errLibraryPanelHeaderUIDMissing.Error())
})
}
type libraryPanel struct { type libraryPanel struct {
ID int64 ID int64
OrgID int64 OrgID int64
@ -783,7 +683,7 @@ func scenarioWithLibraryPanel(t *testing.T, desc string, fn func(t *testing.T, s
"description": "A description" "description": "A description"
} }
`), `),
Kind: int64(libraryelements.Panel), Kind: int64(models.PanelElement),
} }
resp, err := sc.elementService.CreateElement(sc.reqContext, command) resp, err := sc.elementService.CreateElement(sc.reqContext, command)
require.NoError(t, err) require.NoError(t, err)

View File

@ -69,6 +69,7 @@ func (ng *AlertNG) Init() error {
BaseInterval: baseInterval, BaseInterval: baseInterval,
DefaultIntervalSeconds: defaultIntervalSeconds, DefaultIntervalSeconds: defaultIntervalSeconds,
SQLStore: ng.SQLStore, SQLStore: ng.SQLStore,
Logger: ng.Log,
} }
var err error var err error

View File

@ -442,6 +442,8 @@ func (am *Alertmanager) buildReceiverIntegrations(receiver *apimodels.PostableAp
n, err = channels.NewThreemaNotifier(cfg, tmpl) n, err = channels.NewThreemaNotifier(cfg, tmpl)
case "opsgenie": case "opsgenie":
n, err = channels.NewOpsgenieNotifier(cfg, tmpl) n, err = channels.NewOpsgenieNotifier(cfg, tmpl)
case "prometheus-alertmanager":
n, err = channels.NewAlertmanagerNotifier(cfg, tmpl)
default: default:
return nil, fmt.Errorf("notifier %s is not supported", r.Type) return nil, fmt.Errorf("notifier %s is not supported", r.Type)
} }

View File

@ -9,6 +9,8 @@ import (
"testing" "testing"
"time" "time"
"github.com/grafana/grafana/pkg/infra/log"
gokit_log "github.com/go-kit/kit/log" gokit_log "github.com/go-kit/kit/log"
"github.com/go-openapi/strfmt" "github.com/go-openapi/strfmt"
"github.com/prometheus/alertmanager/api/v2/models" "github.com/prometheus/alertmanager/api/v2/models"
@ -42,6 +44,7 @@ func setupAMTest(t *testing.T) *Alertmanager {
BaseInterval: 10 * time.Second, BaseInterval: 10 * time.Second,
DefaultIntervalSeconds: 60, DefaultIntervalSeconds: 60,
SQLStore: sqlStore, SQLStore: sqlStore,
Logger: log.New("alertmanager-test"),
} }
am, err := New(cfg, store, m) am, err := New(cfg, store, m)

Some files were not shown because too many files have changed in this diff Show More