mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge remote-tracking branch 'origin/main' into 34485
This commit is contained in:
commit
51b11fb74c
24
.drone.yml
24
.drone.yml
@ -755,8 +755,8 @@ steps:
|
||||
image: grafana/build-container:1.4.1
|
||||
commands:
|
||||
- "[ $(grep FocusConvey -R pkg | wc -l) -eq \"0\" ] || exit 1"
|
||||
- ./bin/grabpl test-backend --edition oss
|
||||
- ./bin/grabpl integration-tests --edition oss
|
||||
- ./bin/grabpl test-backend --edition oss --tries 5
|
||||
- ./bin/grabpl integration-tests --edition oss --tries 5
|
||||
depends_on:
|
||||
- initialize
|
||||
|
||||
@ -1130,8 +1130,8 @@ steps:
|
||||
image: grafana/build-container:1.4.1
|
||||
commands:
|
||||
- "[ $(grep FocusConvey -R pkg | wc -l) -eq \"0\" ] || exit 1"
|
||||
- ./bin/grabpl test-backend --edition enterprise
|
||||
- ./bin/grabpl integration-tests --edition enterprise
|
||||
- ./bin/grabpl test-backend --edition enterprise --tries 5
|
||||
- ./bin/grabpl integration-tests --edition enterprise --tries 5
|
||||
depends_on:
|
||||
- initialize
|
||||
|
||||
@ -1196,8 +1196,8 @@ steps:
|
||||
image: grafana/build-container:1.4.1
|
||||
commands:
|
||||
- "[ $(grep FocusConvey -R pkg | wc -l) -eq \"0\" ] || exit 1"
|
||||
- ./bin/grabpl test-backend --edition enterprise2
|
||||
- ./bin/grabpl integration-tests --edition enterprise2
|
||||
- ./bin/grabpl test-backend --edition enterprise2 --tries 5
|
||||
- ./bin/grabpl integration-tests --edition enterprise2 --tries 5
|
||||
depends_on:
|
||||
- initialize
|
||||
|
||||
@ -1710,8 +1710,8 @@ steps:
|
||||
image: grafana/build-container:1.4.1
|
||||
commands:
|
||||
- "[ $(grep FocusConvey -R pkg | wc -l) -eq \"0\" ] || exit 1"
|
||||
- ./bin/grabpl test-backend --edition oss
|
||||
- ./bin/grabpl integration-tests --edition oss
|
||||
- ./bin/grabpl test-backend --edition oss --tries 5
|
||||
- ./bin/grabpl integration-tests --edition oss --tries 5
|
||||
depends_on:
|
||||
- initialize
|
||||
|
||||
@ -2074,8 +2074,8 @@ steps:
|
||||
image: grafana/build-container:1.4.1
|
||||
commands:
|
||||
- "[ $(grep FocusConvey -R pkg | wc -l) -eq \"0\" ] || exit 1"
|
||||
- ./bin/grabpl test-backend --edition enterprise
|
||||
- ./bin/grabpl integration-tests --edition enterprise
|
||||
- ./bin/grabpl test-backend --edition enterprise --tries 5
|
||||
- ./bin/grabpl integration-tests --edition enterprise --tries 5
|
||||
depends_on:
|
||||
- initialize
|
||||
|
||||
@ -2140,8 +2140,8 @@ steps:
|
||||
image: grafana/build-container:1.4.1
|
||||
commands:
|
||||
- "[ $(grep FocusConvey -R pkg | wc -l) -eq \"0\" ] || exit 1"
|
||||
- ./bin/grabpl test-backend --edition enterprise2
|
||||
- ./bin/grabpl integration-tests --edition enterprise2
|
||||
- ./bin/grabpl test-backend --edition enterprise2 --tries 5
|
||||
- ./bin/grabpl integration-tests --edition enterprise2 --tries 5
|
||||
depends_on:
|
||||
- initialize
|
||||
|
||||
|
@ -139,10 +139,13 @@ connstr =
|
||||
# This enables data proxy logging, default is 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.
|
||||
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.
|
||||
keep_alive_seconds = 30
|
||||
|
||||
|
@ -145,10 +145,13 @@
|
||||
# This enables data proxy logging, default is 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.
|
||||
;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.
|
||||
;keep_alive_seconds = 30
|
||||
|
||||
|
2
devenv/docker/blocks/slow_proxy_mac/.env
Normal file
2
devenv/docker/blocks/slow_proxy_mac/.env
Normal file
@ -0,0 +1,2 @@
|
||||
ORIGIN_SERVER=http://host.docker.internal:9090/
|
||||
SLEEP_DURATION=60s
|
@ -1,7 +1,10 @@
|
||||
|
||||
FROM golang:latest
|
||||
FROM golang:latest as builder
|
||||
ADD main.go /
|
||||
WORKDIR /
|
||||
RUN GO111MODULE=off go build -o main .
|
||||
RUN GO111MODULE=off CGO_ENABLED=0 go build -o main .
|
||||
|
||||
FROM scratch
|
||||
WORKDIR /
|
||||
EXPOSE 3011
|
||||
COPY --from=builder /main /main
|
||||
ENTRYPOINT ["/main"]
|
||||
|
@ -3,4 +3,5 @@
|
||||
ports:
|
||||
- '3011:3011'
|
||||
environment:
|
||||
ORIGIN_SERVER: 'http://host.docker.internal:9090/'
|
||||
ORIGIN_SERVER: ${ORIGIN_SERVER}
|
||||
SLEEP_DURATION: ${SLEEP_DURATION}
|
@ -1,7 +1,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
@ -13,16 +12,26 @@ import (
|
||||
func main() {
|
||||
origin := os.Getenv("ORIGIN_SERVER")
|
||||
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)
|
||||
proxy := httputil.NewSingleHostReverseProxy(originURL)
|
||||
|
||||
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)
|
||||
proxy.ServeHTTP(w, r)
|
||||
})
|
||||
|
@ -575,3 +575,9 @@ The following sections detail the supported settings and secure settings for eac
|
||||
| Name |
|
||||
| ---- |
|
||||
| url |
|
||||
|
||||
## Grafana Enterprise
|
||||
|
||||
Grafana Enterprise supports provisioning for the following resources:
|
||||
|
||||
- [Access Control Provisioning]({{< relref "../enterprise/access-control/provisioning.md" >}})
|
||||
|
@ -6,39 +6,34 @@ weight = 110
|
||||
|
||||
# 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.
|
||||
- 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.
|
||||
|
||||
Currently only the graph panel visualization supports alerts.
|
||||
- Alert rule - One or more conditions, the frequency of evaluation, and the (optional) duration that a condition must be met before notifying.
|
||||
- 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.
|
||||
- Silences - Date and matching criteria used to silence notifications.
|
||||
|
||||
## Alert tasks
|
||||
|
||||
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" >}})
|
||||
- [View existing alert rules and their current state]({{< relref "view-alerts.md" >}})
|
||||
- [Test alert rules and troubleshoot]({{< relref "troubleshoot-alerts.md" >}})
|
||||
- [Add or edit an alert contact point]({{< relref "notifications.md" >}})
|
||||
|
||||
## 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.
|
||||
|
||||
## 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 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`,
|
||||
Alert rules can only query backend data sources with alerting enabled:
|
||||
- builtin or developed and maintained by grafana: `Graphite`, `Prometheus`, `Loki`, `InfluxDB`, `Elasticsearch`,
|
||||
`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" >}}))
|
||||
|
||||
@ -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" >}}).
|
||||
|
||||
Description | Type | Metric name
|
||||
Metric Name | Type | Description
|
||||
---------- | ----------- | ----------
|
||||
Total number of alerts | counter | `alerting.active_alerts`
|
||||
Alert execution result | counter | `alerting.result`
|
||||
Notifications sent counter | counter | `alerting.notifications_sent`
|
||||
Alert execution timer | timer | `alerting.execution_time`
|
||||
`alerting.alerts` | gauge | How many alerts by state
|
||||
`alerting.request_duration_seconds` | histogram | Histogram of requests to the Alerting API
|
||||
`alerting.active_configurations` | gauge | The number of active, non default alertmanager configurations for grafana managed alerts
|
||||
`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
|
||||
|
@ -1,5 +1,5 @@
|
||||
+++
|
||||
title = "Time series"
|
||||
title = "Intro to time series"
|
||||
description = "Introduction to time series"
|
||||
keywords = ["grafana", "intro", "guide", "concepts", "timeseries"]
|
||||
weight = 400
|
||||
|
@ -43,6 +43,7 @@ With Grafana Enterprise [enhanced LDAP]({{< relref "enhanced_ldap.md" >}}), you
|
||||
|
||||
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.
|
||||
- [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" >}})
|
||||
|
60
docs/sources/enterprise/access-control/_index.md
Normal file
60
docs/sources/enterprise/access-control/_index.md
Normal 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" >}}).
|
75
docs/sources/enterprise/access-control/permissions.md
Normal file
75
docs/sources/enterprise/access-control/permissions.md
Normal 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.
|
140
docs/sources/enterprise/access-control/provisioning.md
Normal file
140
docs/sources/enterprise/access-control/provisioning.md
Normal 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
|
101
docs/sources/enterprise/access-control/roles.md
Normal file
101
docs/sources/enterprise/access-control/roles.md
Normal 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 can’t 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 won’t 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.
|
226
docs/sources/enterprise/access-control/usage-scenarios.md
Normal file
226
docs/sources/enterprise/access-control/usage-scenarios.md
Normal 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.
|
@ -2,7 +2,7 @@
|
||||
title = "Auditing"
|
||||
description = "Auditing"
|
||||
keywords = ["grafana", "auditing", "audit", "logs"]
|
||||
weight = 700
|
||||
weight = 1100
|
||||
+++
|
||||
|
||||
# Auditing
|
||||
|
@ -2,7 +2,7 @@
|
||||
title = "Data source permissions"
|
||||
description = "Grafana Datasource Permissions Guide "
|
||||
keywords = ["grafana", "configuration", "documentation", "datasource", "permissions", "users", "teams", "enterprise"]
|
||||
weight = 200
|
||||
weight = 500
|
||||
+++
|
||||
|
||||
# Data source permissions
|
||||
|
@ -2,7 +2,7 @@
|
||||
title = "Enhanced LDAP Integration"
|
||||
description = "Grafana Enhanced LDAP Integration Guide "
|
||||
keywords = ["grafana", "configuration", "documentation", "ldap", "active directory", "enterprise"]
|
||||
weight = 300
|
||||
weight = 600
|
||||
+++
|
||||
|
||||
# Enhanced LDAP integration
|
||||
|
@ -2,7 +2,7 @@
|
||||
title = "Enterprise configuration"
|
||||
description = "Enterprise configuration documentation"
|
||||
keywords = ["grafana", "configuration", "documentation", "enterprise"]
|
||||
weight = 300
|
||||
weight = 700
|
||||
+++
|
||||
|
||||
# Grafana Enterprise configuration
|
||||
|
@ -2,7 +2,7 @@
|
||||
title = "Export dashboard as PDF"
|
||||
description = ""
|
||||
keywords = ["grafana", "export", "pdf", "share"]
|
||||
weight = 900
|
||||
weight = 1400
|
||||
+++
|
||||
|
||||
# Export dashboard as PDF
|
||||
|
@ -2,7 +2,7 @@
|
||||
title = "Grafana Enterprise license"
|
||||
description = "Enterprise license"
|
||||
keywords = ["grafana", "licensing", "enterprise"]
|
||||
weight = 100
|
||||
weight = 10
|
||||
+++
|
||||
|
||||
# Grafana Enterprise license
|
||||
|
@ -2,7 +2,7 @@
|
||||
title = "Query caching"
|
||||
description = "Grafana Enterprise data source query caching"
|
||||
keywords = ["grafana", "plugins", "query", "caching"]
|
||||
weight = 110
|
||||
weight = 300
|
||||
+++
|
||||
|
||||
# Query caching
|
||||
|
@ -3,7 +3,7 @@ title = "Reporting"
|
||||
description = ""
|
||||
keywords = ["grafana", "reporting"]
|
||||
aliases = ["/docs/grafana/latest/administration/reports"]
|
||||
weight = 400
|
||||
weight = 800
|
||||
+++
|
||||
|
||||
# Reporting
|
||||
@ -12,6 +12,9 @@ Reporting allows you to automatically generate PDFs from any of your dashboards
|
||||
|
||||
> 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" >}}
|
||||
|
||||
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.
|
||||
- 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
|
||||
|
||||
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. Enter report information. All fields are required unless otherwise indicated.
|
||||
|
@ -2,7 +2,7 @@
|
||||
title = "Request security"
|
||||
description = "Grafana Enterprise request security"
|
||||
keywords = ["grafana", "security", "enterprise"]
|
||||
weight = 110
|
||||
weight = 400
|
||||
+++
|
||||
|
||||
# Request security
|
||||
|
@ -3,7 +3,7 @@ title = "SAML Authentication"
|
||||
description = "Grafana SAML Authentication"
|
||||
keywords = ["grafana", "saml", "documentation", "saml-auth"]
|
||||
aliases = ["/docs/grafana/latest/auth/saml/"]
|
||||
weight = 500
|
||||
weight = 900
|
||||
+++
|
||||
|
||||
# SAML authentication
|
||||
|
@ -3,7 +3,7 @@ title = "Team sync"
|
||||
description = "Grafana Team Sync"
|
||||
keywords = ["grafana", "auth", "documentation"]
|
||||
aliases = ["/docs/grafana/latest/auth/saml/"]
|
||||
weight = 600
|
||||
weight = 1000
|
||||
+++
|
||||
|
||||
# Team sync
|
||||
|
@ -3,7 +3,7 @@ title = "Usage insights"
|
||||
description = "Understand how your Grafana instance is used"
|
||||
keywords = ["grafana", "usage-insights", "enterprise"]
|
||||
aliases = ["/docs/grafana/latest/enterprise/usage-insights/"]
|
||||
weight = 100
|
||||
weight = 200
|
||||
+++
|
||||
|
||||
# Usage insights
|
||||
|
@ -2,7 +2,7 @@
|
||||
title = "Vault"
|
||||
description = ""
|
||||
keywords = ["grafana", "vault", "configuration"]
|
||||
weight = 700
|
||||
weight = 1200
|
||||
+++
|
||||
|
||||
# Vault integration
|
||||
|
@ -3,7 +3,7 @@ title = "White labeling"
|
||||
description = "Change the look of Grafana to match your corporate brand"
|
||||
keywords = ["grafana", "white-labeling", "enterprise"]
|
||||
aliases = ["/docs/grafana/latest/enterprise/white-labeling/"]
|
||||
weight = 700
|
||||
weight = 1300
|
||||
+++
|
||||
|
||||
# White labeling
|
||||
|
@ -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
|
||||
|
||||
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).
|
||||
|
||||
|
@ -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. 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.
|
||||
|
||||
> **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:
|
||||
|
||||
1. Click the **+** icon on the left panel, select **Create Dashboard**, and then click **Add an empty panel**.
|
||||
1. In the New Dashboard/Edit Panel view, go to the **Query** tab.
|
||||
1. Click the **+** icon on the side menu.
|
||||
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. 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**.
|
||||
|
@ -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:
|
||||
|
||||
- [Fine-Grained Access Control API]({{< relref "access_control.md" >}})
|
||||
- [Data Source Permissions API]({{< relref "datasource_permissions.md" >}})
|
||||
- [External Group Sync API]({{< relref "external_group_sync.md" >}})
|
||||
- [License API]({{< relref "licensing.md" >}})
|
||||
|
584
docs/sources/http_api/access_control.md
Normal file
584
docs/sources/http_api/access_control.md
Normal 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.
|
@ -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
|
||||
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
|
||||
|
||||
`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.
|
||||
|
||||
#### Required permissions
|
||||
|
||||
See note in the [introduction]({{< ref "#admin-api" >}}) for an explanation.
|
||||
|
||||
Action | Scope
|
||||
--- | --- |
|
||||
users:create | n/a
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```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.
|
||||
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**:
|
||||
|
||||
```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.
|
||||
|
||||
#### Required permissions
|
||||
|
||||
See note in the [introduction]({{< ref "#admin-api" >}}) for an explanation.
|
||||
|
||||
Action | Scope
|
||||
--- | --- |
|
||||
users.permissions:update | global:users:*
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```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.
|
||||
|
||||
#### Required permissions
|
||||
|
||||
See note in the [introduction]({{< ref "#admin-api" >}}) for an explanation.
|
||||
|
||||
Action | Scope
|
||||
--- | --- |
|
||||
users:delete | global:users:*
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```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.
|
||||
|
||||
#### Required permissions
|
||||
|
||||
See note in the [introduction]({{< ref "#admin-api" >}}) for an explanation.
|
||||
|
||||
Action | Scope
|
||||
--- | --- |
|
||||
users.authtoken:list | global:users:*
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```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.
|
||||
|
||||
#### Required permissions
|
||||
|
||||
See note in the [introduction]({{< ref "#admin-api" >}}) for an explanation.
|
||||
|
||||
Action | Scope
|
||||
--- | --- |
|
||||
users.authtoken:update | global:users:*
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```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.
|
||||
|
||||
#### Required permissions
|
||||
|
||||
See note in the [introduction]({{< ref "#admin-api" >}}) for an explanation.
|
||||
|
||||
Action | Scope
|
||||
--- | --- |
|
||||
users.logout | global:users:*
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
@ -465,12 +524,22 @@ Content-Type: application/json
|
||||
|
||||
`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
|
||||
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.
|
||||
|
||||
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**:
|
||||
|
||||
```http
|
||||
|
@ -5,13 +5,15 @@ keywords = ["grafana", "http", "documentation", "api", "organization"]
|
||||
aliases = ["/docs/grafana/latest/http_api/organization/"]
|
||||
+++
|
||||
|
||||
|
||||
# Organization API
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
### Get current Organization
|
||||
@ -46,6 +48,14 @@ Content-Type: application/json
|
||||
Returns all org users within the current organization.
|
||||
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**:
|
||||
|
||||
```http
|
||||
@ -112,6 +122,14 @@ Content-Type: application/json
|
||||
|
||||
`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**:
|
||||
|
||||
```http
|
||||
@ -138,6 +156,14 @@ Content-Type: application/json
|
||||
|
||||
`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**:
|
||||
|
||||
```http
|
||||
@ -188,6 +214,14 @@ Content-Type: application/json
|
||||
|
||||
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**:
|
||||
|
||||
```http
|
||||
@ -407,6 +441,14 @@ Content-Type: application/json
|
||||
|
||||
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**:
|
||||
|
||||
```http
|
||||
@ -440,6 +482,14 @@ Content-Type: application/json
|
||||
|
||||
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**:
|
||||
|
||||
```http
|
||||
@ -468,6 +518,14 @@ Content-Type: application/json
|
||||
|
||||
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**:
|
||||
|
||||
```http
|
||||
@ -495,6 +553,14 @@ Content-Type: application/json
|
||||
|
||||
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**:
|
||||
|
||||
```http
|
||||
|
@ -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" >}}).
|
||||
|
||||
> 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
|
||||
|
||||
@ -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.
|
||||
|
||||
#### Required permissions
|
||||
|
||||
See note in the [introduction]({{< ref "#reporting-api" >}}) for an explanation.
|
||||
|
||||
Action | Scope
|
||||
--- | --- |
|
||||
reports:send | n/a
|
||||
|
||||
### Example request
|
||||
|
||||
```http
|
||||
@ -63,4 +73,4 @@ Code | Description
|
||||
401 | Authentication failed, refer to [Authentication API]({{< relref "../http_api/auth.md" >}}).
|
||||
403 | User is authenticated but is not authorized to generate the report.
|
||||
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.
|
||||
|
@ -5,12 +5,23 @@ keywords = ["grafana", "http", "documentation", "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
|
||||
|
||||
`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**:
|
||||
|
||||
```http
|
||||
@ -58,6 +69,14 @@ Content-Type: application/json
|
||||
|
||||
`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**:
|
||||
|
||||
```http
|
||||
@ -111,6 +130,14 @@ Content-Type: application/json
|
||||
|
||||
`GET /api/users/:id`
|
||||
|
||||
#### Required permissions
|
||||
|
||||
See note in the [introduction]({{< ref "#user-api" >}}) for an explanation.
|
||||
|
||||
Action | Scope
|
||||
--- | --- |
|
||||
users:read | users:*
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
@ -148,6 +175,14 @@ Content-Type: application/json
|
||||
|
||||
`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**:
|
||||
|
||||
```http
|
||||
@ -195,6 +230,14 @@ Content-Type: application/json
|
||||
|
||||
`PUT /api/users/:id`
|
||||
|
||||
#### Required permissions
|
||||
|
||||
See note in the [introduction]({{< ref "#user-api" >}}) for an explanation.
|
||||
|
||||
Action | Scope
|
||||
--- | --- |
|
||||
users:write | users:*
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
@ -226,6 +269,14 @@ Content-Type: application/json
|
||||
|
||||
`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**:
|
||||
|
||||
```http
|
||||
@ -256,6 +307,14 @@ Content-Type: application/json
|
||||
|
||||
`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**:
|
||||
|
||||
```http
|
||||
|
@ -8,7 +8,7 @@ weight = 300
|
||||
|
||||
# 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.
|
||||
|
||||
|
@ -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.
|
||||
|
||||
> 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
|
||||
|
||||
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.
|
||||
|
@ -1,58 +1,24 @@
|
||||
+++
|
||||
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
|
||||
|
||||
# General
|
||||
Add a panel
|
||||
Panel editor
|
||||
|
||||
# Work with your metrics
|
||||
Queries
|
||||
- Share query results
|
||||
- Mixed data source queries
|
||||
Expressions
|
||||
- Mixed data source queries (not yet written)
|
||||
Expressions (beta)
|
||||
Transformations
|
||||
Inspect a panel
|
||||
|
||||
# Adjust appearance
|
||||
|
||||
Panel options
|
||||
Visualization-specific settings (Visualizations)
|
||||
Thresholds
|
||||
Value mappings
|
||||
Data links (can be for all fields or one)
|
||||
|
||||
Visualizations > specific options
|
||||
|
||||
Overrides
|
||||
|
||||
|
||||
|
@ -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**.
|
||||
|
||||
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
|
||||
|
||||
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" >}}).
|
||||
|
||||

|
||||
|
||||
**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
|
||||
## 2. 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" >}}).
|
||||
|
||||
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.
|
||||
|
||||
## 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" >}}).
|
||||

|
||||
|
||||
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
|
||||
|
||||
@ -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.
|
||||
- Add more [queries]({{< relref "queries.md" >}}).
|
||||
- [Transform]({{< relref "transformations/_index.md" >}}) your data.
|
||||
- [Configure]({{< relref "field-options/_index.md" >}}) how your results are displayed in the visualization.
|
||||
|
||||
- If you made a graph panel, set up an [alert]({{< relref "../alerting/_index.md" >}}).
|
||||
- Set up an [alert]({{< relref "../alerting/_index.md" >}}).
|
||||
- Create [templates and variables]({{< relref "../variables/_index.md" >}}).
|
||||
|
@ -1,7 +1,7 @@
|
||||
+++
|
||||
title = "Legend options"
|
||||
aliases = ["/docs/grafana/latest/panels/visualizations/panel-legend/"]
|
||||
weight = 500
|
||||
weight = 950
|
||||
+++
|
||||
|
||||
# Legend options
|
||||
|
@ -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:
|
||||
|
||||
- [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" >}}).
|
||||
- [Panel links]({{< relref "../linking/panel-links.md" >}}) and [Data links]({{< relref "../linking/data-links.md" >}}) help you connect your visualization to other resources.
|
||||
|
||||
|
38
docs/sources/panels/panel-options.md
Normal file
38
docs/sources/panels/panel-options.md
Normal 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.
|
||||
|
||||

|
||||
|
||||
## 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" >}}).
|
@ -1,12 +1,13 @@
|
||||
+++
|
||||
title = "Repeat panels or rows"
|
||||
keywords = ["grafana", "templating", "documentation", "guide", "template", "variable", "repeat"]
|
||||
aliases = ["/docs/grafana/latest/variables/repeat-panels-or-rows/"]
|
||||
weight = 800
|
||||
+++
|
||||
|
||||
# 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
|
||||
Grafana to dynamically create new panels or rows based on what values you have selected, you can use the _Repeat_ feature.
|
@ -1,12 +1,10 @@
|
||||
+++
|
||||
title = "Visualizations"
|
||||
title = "Visualization options"
|
||||
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.
|
||||
|
||||
The default options and their unique display options are described in the pages in this section.
|
||||
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.
|
||||
|
||||
You can add more panel types with [plugins]({{< relref "../../plugins/_index.md" >}}).
|
||||
|
@ -8,6 +8,8 @@ weight = 50
|
||||
|
||||
# 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.
|
||||
|
||||
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 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 administrators have the **Grafana Admin** flag enabled on their account. They can access the **Server Admin** menu and perform the following tasks:
|
||||
|
@ -7,6 +7,8 @@ weight = 100
|
||||
|
||||
# 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.
|
||||
|
||||
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 | | |
|
||||
| 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
|
||||
|
||||
Can do everything scoped to the organization. For example:
|
||||
|
@ -5,6 +5,8 @@ weight = 500
|
||||
|
||||
# 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).
|
||||
|
||||
- You cannot override permissions for users with the Organization Admin role. Admins always have access to everything.
|
||||
|
@ -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
|
||||
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 7.5.7]({{< relref "release-notes-7-5-7" >}})
|
||||
- [Release notes for 7.5.6]({{< relref "release-notes-7-5-6" >}})
|
||||
|
5
docs/sources/shared/panels/panel-links-intro.md
Normal file
5
docs/sources/shared/panels/panel-links-intro.md
Normal 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.
|
5
docs/sources/shared/panels/repeat-panel-intro.md
Normal file
5
docs/sources/shared/panels/repeat-panel-intro.md
Normal 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.
|
@ -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.
|
||||
|
||||
### 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)**.
|
||||
- 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.
|
||||
- We added support for a shared crosshair and a tooltip that’s now smarter when it comes to data display in the tooltip.
|
||||
- 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
|
||||
|
||||
Lots of panel editor improvements, heavily informed by user research and community feedback.
|
||||
|
||||
- All options are now shown in a single pane.
|
||||
- You can now search panel options.
|
||||
- Value mapping has been completely redesigned.
|
||||
- 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
|
||||
|
||||
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.
|
||||
|
||||
#### 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
|
||||
|
||||
These features are included in the Grafana Enterprise edition.
|
||||
|
13
go.mod
13
go.mod
@ -14,8 +14,8 @@ replace k8s.io/client-go => k8s.io/client-go v0.18.8
|
||||
require (
|
||||
cloud.google.com/go/storage v1.14.0
|
||||
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/azidentity v0.8.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.16.1
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.9.1
|
||||
github.com/BurntSushi/toml v0.3.1
|
||||
github.com/Masterminds/semver v1.5.0
|
||||
github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f
|
||||
@ -51,8 +51,8 @@ require (
|
||||
github.com/gosimple/slug v1.9.0
|
||||
github.com/grafana/grafana-aws-sdk v0.4.0
|
||||
github.com/grafana/grafana-live-sdk v0.0.6
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.99.0
|
||||
github.com/grafana/loki v1.6.2-0.20210510132741-f408e05ad426
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.100.0
|
||||
github.com/grafana/loki v1.6.2-0.20210520072447-15d417efe103
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
|
||||
github.com/hashicorp/go-hclog v0.16.0
|
||||
github.com/hashicorp/go-plugin v1.4.0
|
||||
@ -62,6 +62,7 @@ require (
|
||||
github.com/jmespath/go-jmespath v0.4.0
|
||||
github.com/json-iterator/go v1.1.11
|
||||
github.com/jung-kurt/gofpdf v1.16.2
|
||||
github.com/laher/mergefs v0.1.1
|
||||
github.com/lib/pq v1.10.0
|
||||
github.com/linkedin/goavro/v2 v2.10.0
|
||||
github.com/magefile/mage v1.11.0
|
||||
@ -93,10 +94,10 @@ require (
|
||||
go.opentelemetry.io/collector v0.25.0
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a
|
||||
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/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/tools v0.1.0
|
||||
gonum.org/v1/gonum v0.9.1
|
||||
|
28
go.sum
28
go.sum
@ -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/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.16.0 h1:ZsS7JltN+5D42mcU3Mb4lwVivlFL89v+FlXXMXE2YEM=
|
||||
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/azidentity v0.8.0 h1:wb00szFWtKeIef2Q5X8gdd0mYp8oSHmJOYUh/QXD8sw=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.8.0/go.mod h1:acANgl9stsT5xflESXKjZx4rhZJSr0TGgTDYY0xJPIE=
|
||||
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.1/go.mod h1:MVdrcUC4Hup35qHym3VdzoW+NBgBxrta9Vei97jRtM8=
|
||||
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.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.1 h1:vx8McI56N5oLSQu8xa+xdiE0fjQq8W8Zt49vHP8Rygw=
|
||||
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-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.99.0 h1:pEmoSSYw7VsF+rhRgG4z+azE3eLwznomxVg9Ezppqzo=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.99.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.20210510132741-f408e05ad426/go.mod h1:IfQ9BWq2sVAk3iKB4Pahz6QNTs5D4WpfJj/AY8xzmNw=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.100.0 h1:BryvIFdx/HrsKMt2hkxN7cJ0WrCgKpgjdJW8y8TSol0=
|
||||
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.20210520072447-15d417efe103 h1:qCmofFVwQR9QnsinstVqI1NPLMVl33jNCnOCXEAVn6E=
|
||||
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/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=
|
||||
@ -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/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/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-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
|
||||
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/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/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/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=
|
||||
@ -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-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-20210510120150-4163338589ed h1:p9UgmWI9wKpfYmgaV/IZKGdXc5qEK45tDwwwDyjS26I=
|
||||
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210521195947-fe42d452be8f h1:Si4U+UcgJzya9kpiEUJKQvjr512OLli+gL4poHrz93U=
|
||||
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-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
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-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-20210514084401-e8d321eab015 h1:hZR0X1kPW+nwyJ9xRxqZk1vx5RUObAPBdKVvXPDUH/E=
|
||||
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210521203332-0cec03c779c1 h1:lCnv+lfrU9FRPGf8NeRuWAAPjNnema5WtBinMgs1fD8=
|
||||
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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
@ -1,4 +1,4 @@
|
||||
{
|
||||
"stable": "7.5.7",
|
||||
"testing": "8.0.0-beta1"
|
||||
"testing": "8.0.0-beta2"
|
||||
}
|
||||
|
@ -103,7 +103,6 @@
|
||||
"@types/file-saver": "2.0.1",
|
||||
"@types/history": "^4.7.8",
|
||||
"@types/hoist-non-react-statics": "3.3.1",
|
||||
"@types/is-hotkey": "0.1.1",
|
||||
"@types/jest": "26.0.15",
|
||||
"@types/jquery": "3.3.38",
|
||||
"@types/jsurl": "^1.2.28",
|
||||
@ -256,8 +255,6 @@
|
||||
"history": "4.10.1",
|
||||
"hoist-non-react-statics": "3.3.2",
|
||||
"immer": "8.0.1",
|
||||
"immutable": "3.8.2",
|
||||
"is-hotkey": "0.1.6",
|
||||
"jquery": "3.5.1",
|
||||
"json-source-map": "0.6.1",
|
||||
"jsurl": "^0.1.5",
|
||||
@ -281,7 +278,6 @@
|
||||
"react-dom": "17.0.1",
|
||||
"react-grid-layout": "1.2.0",
|
||||
"react-highlight-words": "0.17.0",
|
||||
"react-inlinesvg": "2.3.0",
|
||||
"react-loadable": "5.5.0",
|
||||
"react-popper": "2.2.4",
|
||||
"react-redux": "7.2.0",
|
||||
@ -312,7 +308,8 @@
|
||||
"whatwg-fetch": "3.1.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"caniuse-db": "1.0.30000772"
|
||||
"caniuse-db": "1.0.30000772",
|
||||
"underscore": "1.12.1"
|
||||
},
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
|
@ -45,6 +45,7 @@
|
||||
"d3": "5.15.0",
|
||||
"hoist-non-react-statics": "3.3.2",
|
||||
"immutable": "3.8.2",
|
||||
"is-hotkey": "0.1.6",
|
||||
"jquery": "3.5.1",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.29.1",
|
||||
@ -62,6 +63,7 @@
|
||||
"react-dom": "17.0.1",
|
||||
"react-highlight-words": "0.16.0",
|
||||
"react-hook-form": "7.5.3",
|
||||
"react-inlinesvg": "2.3.0",
|
||||
"react-popper": "2.2.4",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-select": "4.3.0",
|
||||
@ -86,6 +88,7 @@
|
||||
"@types/common-tags": "^1.8.0",
|
||||
"@types/d3": "5.7.2",
|
||||
"@types/hoist-non-react-statics": "3.3.1",
|
||||
"@types/is-hotkey": "0.1.1",
|
||||
"@types/jest": "26.0.15",
|
||||
"@types/jquery": "3.3.38",
|
||||
"@types/lodash": "4.14.123",
|
||||
|
@ -2,10 +2,12 @@ import React, { FC, useCallback, useEffect, useRef } from 'react';
|
||||
import { isNil } from 'lodash';
|
||||
import classNames from 'classnames';
|
||||
import { css } from '@emotion/css';
|
||||
import Scrollbars from 'react-custom-scrollbars';
|
||||
import Scrollbars, { positionValues } from 'react-custom-scrollbars';
|
||||
import { useStyles2 } from '../../themes';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
export type ScrollbarPosition = positionValues;
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
autoHide?: boolean;
|
||||
@ -15,7 +17,7 @@ interface Props {
|
||||
hideHorizontalTrack?: boolean;
|
||||
hideVerticalTrack?: boolean;
|
||||
scrollTop?: number;
|
||||
setScrollTop?: (event: any) => void;
|
||||
setScrollTop?: (position: ScrollbarPosition) => void;
|
||||
autoHeightMin?: number | string;
|
||||
updateAfterMountMs?: number;
|
||||
}
|
||||
@ -101,11 +103,15 @@ export const CustomScrollbar: FC<Props> = ({
|
||||
return <div {...passedProps} className="scrollbar-view" />;
|
||||
}, []);
|
||||
|
||||
const onScrollStop = useCallback(() => {
|
||||
ref.current && setScrollTop && setScrollTop(ref.current.getValues());
|
||||
}, [setScrollTop]);
|
||||
|
||||
return (
|
||||
<Scrollbars
|
||||
ref={ref}
|
||||
className={classNames(styles.customScrollbar, className)}
|
||||
onScroll={setScrollTop}
|
||||
onScrollStop={onScrollStop}
|
||||
autoHeight={true}
|
||||
autoHide={autoHide}
|
||||
autoHideTimeout={autoHideTimeout}
|
||||
|
@ -24,7 +24,7 @@ export function Form<T>({
|
||||
maxWidth = 600,
|
||||
...htmlProps
|
||||
}: FormProps<T>) {
|
||||
const { handleSubmit, register, control, trigger, getValues, formState, watch, setValue } = useForm<T>({
|
||||
const { handleSubmit, trigger, formState, ...rest } = useForm<T>({
|
||||
mode: validateOn,
|
||||
defaultValues,
|
||||
});
|
||||
@ -45,7 +45,7 @@ export function Form<T>({
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
{...htmlProps}
|
||||
>
|
||||
{children({ register, errors: formState.errors, control, getValues, formState, watch, setValue })}
|
||||
{children({ errors: formState.errors, formState, ...rest })}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
@ -23,6 +23,10 @@ export type Props = {
|
||||
inline?: boolean;
|
||||
size?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const Spinner: FC<Props> = (props: Props) => {
|
||||
const { className, inline = false, iconClassName, style, size = 16 } = props;
|
||||
const styles = getStyles(size, inline);
|
||||
|
@ -6,7 +6,7 @@ export { Tooltip, PopoverContent } from './Tooltip/Tooltip';
|
||||
export { PopoverController } from './Tooltip/PopoverController';
|
||||
export { Popover } from './Tooltip/Popover';
|
||||
export { Portal } from './Portal/Portal';
|
||||
export { CustomScrollbar } from './CustomScrollbar/CustomScrollbar';
|
||||
export { CustomScrollbar, ScrollbarPosition } from './CustomScrollbar/CustomScrollbar';
|
||||
export { TabbedContainer, TabConfig } from './TabbedContainer/TabbedContainer';
|
||||
|
||||
export { ClipboardButton } from './ClipboardButton/ClipboardButton';
|
||||
|
@ -1,10 +1,7 @@
|
||||
import { UseFormReturn, FieldValues, FieldErrors } from 'react-hook-form';
|
||||
export { SubmitHandler as FormsOnSubmit, FieldErrors as FormFieldErrors } from 'react-hook-form';
|
||||
|
||||
export type FormAPI<T> = Pick<
|
||||
UseFormReturn<T>,
|
||||
'register' | 'control' | 'formState' | 'getValues' | 'watch' | 'setValue'
|
||||
> & {
|
||||
export type FormAPI<T> = Omit<UseFormReturn<T>, 'trigger' | 'handleSubmit'> & {
|
||||
errors: FieldErrors<T>;
|
||||
};
|
||||
|
||||
|
@ -1348,10 +1348,6 @@ func (m *mockLibraryPanelService) ConnectLibraryPanelsForDashboard(c *models.Req
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockLibraryPanelService) ImportDashboard(c *models.ReqContext, dashboard *simplejson.Json, importedID int64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type mockLibraryElementService struct {
|
||||
}
|
||||
|
||||
|
@ -285,16 +285,18 @@ func getPanelSort(id string) int {
|
||||
sort = 10
|
||||
case "status-grid":
|
||||
sort = 11
|
||||
case "graph":
|
||||
case "histogram":
|
||||
sort = 12
|
||||
case "text":
|
||||
case "graph":
|
||||
sort = 13
|
||||
case "alertlist":
|
||||
case "text":
|
||||
sort = 14
|
||||
case "dashlist":
|
||||
case "alertlist":
|
||||
sort = 15
|
||||
case "news":
|
||||
case "dashlist":
|
||||
sort = 16
|
||||
case "news":
|
||||
sort = 17
|
||||
}
|
||||
return sort
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -15,6 +16,7 @@ type AccessToken struct {
|
||||
|
||||
type TokenCredential interface {
|
||||
GetCacheKey() string
|
||||
Init() error
|
||||
GetAccessToken(ctx context.Context, scopes []string) (*AccessToken, error)
|
||||
}
|
||||
|
||||
@ -31,6 +33,9 @@ type tokenCacheImpl struct {
|
||||
}
|
||||
type credentialCacheEntry struct {
|
||||
credential TokenCredential
|
||||
|
||||
credInit uint32
|
||||
credMutex sync.Mutex
|
||||
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) {
|
||||
return c.getEntryFor(credential).getAccessToken(ctx, scopes)
|
||||
}
|
||||
|
||||
func (c *tokenCacheImpl) getEntryFor(credential TokenCredential) *credentialCacheEntry {
|
||||
var entry interface{}
|
||||
var ok bool
|
||||
|
||||
credentialKey := credential.GetCacheKey()
|
||||
scopesKey := getKeyForScopes(scopes)
|
||||
key := credential.GetCacheKey()
|
||||
|
||||
if entry, ok = c.cache.Load(credentialKey); !ok {
|
||||
entry, _ = c.cache.LoadOrStore(credentialKey, &credentialCacheEntry{
|
||||
if entry, ok = c.cache.Load(key); !ok {
|
||||
entry, _ = c.cache.LoadOrStore(key, &credentialCacheEntry{
|
||||
credential: credential,
|
||||
})
|
||||
}
|
||||
|
||||
credentialEntry := entry.(*credentialCacheEntry)
|
||||
return entry.(*credentialCacheEntry)
|
||||
}
|
||||
|
||||
if entry, ok = credentialEntry.cache.Load(scopesKey); !ok {
|
||||
entry, _ = credentialEntry.cache.LoadOrStore(scopesKey, &scopesCacheEntry{
|
||||
credential: credentialEntry.credential,
|
||||
func (c *credentialCacheEntry) getAccessToken(ctx context.Context, scopes []string) (string, error) {
|
||||
err := c.ensureInitialized()
|
||||
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,
|
||||
cond: sync.NewCond(&sync.Mutex{}),
|
||||
})
|
||||
}
|
||||
|
||||
scopesEntry := entry.(*scopesCacheEntry)
|
||||
|
||||
return scopesEntry.getAccessToken(ctx)
|
||||
return entry.(*scopesCacheEntry)
|
||||
}
|
||||
|
||||
func (c *scopesCacheEntry) getAccessToken(ctx context.Context) (string, error) {
|
||||
|
@ -14,7 +14,9 @@ import (
|
||||
|
||||
type fakeCredential struct {
|
||||
key string
|
||||
initCalledTimes int
|
||||
calledTimes int
|
||||
initFunc func() error
|
||||
getAccessTokenFunc func(ctx context.Context, scopes []string) (*AccessToken, error)
|
||||
}
|
||||
|
||||
@ -22,6 +24,19 @@ func (c *fakeCredential) GetCacheKey() string {
|
||||
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) {
|
||||
c.calledTimes = c.calledTimes + 1
|
||||
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) {
|
||||
ctx := context.Background()
|
||||
|
||||
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{
|
||||
getAccessTokenFunc: func(ctx context.Context, scopes []string) (*AccessToken, error) {
|
||||
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) {
|
||||
credential.calledTimes = 0
|
||||
credential.Reset()
|
||||
|
||||
cacheEntry := &scopesCacheEntry{
|
||||
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
|
||||
credential := &fakeCredential{
|
||||
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{
|
||||
getAccessTokenFunc: func(ctx context.Context, scopes []string) (*AccessToken, error) {
|
||||
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) {
|
||||
credential.calledTimes = 0
|
||||
credential.Reset()
|
||||
|
||||
cacheEntry := &scopesCacheEntry{
|
||||
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
|
||||
credential := &fakeCredential{
|
||||
getAccessTokenFunc: func(ctx context.Context, scopes []string) (*AccessToken, error) {
|
||||
|
@ -3,11 +3,8 @@ package pluginproxy
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
|
||||
@ -109,8 +106,7 @@ func (provider *azureAccessTokenProvider) resolveAuthorityHost(cloudName string)
|
||||
|
||||
type managedIdentityCredential struct {
|
||||
clientId string
|
||||
credLock sync.Mutex
|
||||
credValue atomic.Value // of azcore.TokenCredential
|
||||
credential azcore.TokenCredential
|
||||
}
|
||||
|
||||
func (c *managedIdentityCredential) GetCacheKey() string {
|
||||
@ -121,39 +117,17 @@ func (c *managedIdentityCredential) GetCacheKey() string {
|
||||
return fmt.Sprintf("azure|msi|%s", clientId)
|
||||
}
|
||||
|
||||
func (c *managedIdentityCredential) getCredential() (azcore.TokenCredential, error) {
|
||||
credential := c.credValue.Load()
|
||||
|
||||
if credential == nil {
|
||||
c.credLock.Lock()
|
||||
defer c.credLock.Unlock()
|
||||
|
||||
var err error
|
||||
credential, err = azidentity.NewManagedIdentityCredential(c.clientId, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func (c *managedIdentityCredential) Init() error {
|
||||
if credential, err := azidentity.NewManagedIdentityCredential(c.clientId, nil); err != nil {
|
||||
return err
|
||||
} else {
|
||||
c.credential = credential
|
||||
return nil
|
||||
}
|
||||
|
||||
c.credValue.Store(credential)
|
||||
}
|
||||
|
||||
return credential.(azcore.TokenCredential), nil
|
||||
}
|
||||
|
||||
func (c *managedIdentityCredential) GetAccessToken(ctx context.Context, scopes []string) (*AccessToken, error) {
|
||||
credential, err := c.getCredential()
|
||||
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})
|
||||
accessToken, err := c.credential.GetToken(ctx, azcore.TokenRequestOptions{Scopes: scopes})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -166,40 +140,25 @@ type clientSecretCredential struct {
|
||||
tenantId string
|
||||
clientId string
|
||||
clientSecret string
|
||||
credLock sync.Mutex
|
||||
credValue atomic.Value // of azcore.TokenCredential
|
||||
credential azcore.TokenCredential
|
||||
}
|
||||
|
||||
func (c *clientSecretCredential) GetCacheKey() string {
|
||||
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) {
|
||||
credential := c.credValue.Load()
|
||||
|
||||
if credential == nil {
|
||||
c.credLock.Lock()
|
||||
defer c.credLock.Unlock()
|
||||
|
||||
var err error
|
||||
credential, err = azidentity.NewClientSecretCredential(c.tenantId, c.clientId, c.clientSecret, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func (c *clientSecretCredential) Init() error {
|
||||
options := &azidentity.ClientSecretCredentialOptions{AuthorityHost: c.authority}
|
||||
if credential, err := azidentity.NewClientSecretCredential(c.tenantId, c.clientId, c.clientSecret, options); err != nil {
|
||||
return err
|
||||
} else {
|
||||
c.credential = credential
|
||||
return nil
|
||||
}
|
||||
|
||||
c.credValue.Store(credential)
|
||||
}
|
||||
|
||||
return credential.(azcore.TokenCredential), nil
|
||||
}
|
||||
|
||||
func (c *clientSecretCredential) GetAccessToken(ctx context.Context, scopes []string) (*AccessToken, error) {
|
||||
credential, err := c.getCredential()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accessToken, err := credential.GetToken(ctx, azcore.TokenRequestOptions{Scopes: scopes})
|
||||
accessToken, err := c.credential.GetToken(ctx, azcore.TokenRequestOptions{Scopes: scopes})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -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)
|
||||
if err != nil {
|
||||
return hs.dashboardSaveErrorToApiResponse(err)
|
||||
}
|
||||
|
||||
err = hs.LibraryPanelService.ImportDashboard(c, apiCmd.Dashboard, dashInfo.DashboardId)
|
||||
err = hs.LibraryPanelService.ConnectLibraryPanelsForDashboard(c, dash)
|
||||
if err != nil {
|
||||
return response.Error(500, "Error while connecting library panels", err)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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)
|
||||
})
|
||||
}
|
@ -8,6 +8,7 @@ import (
|
||||
"testing/fstest"
|
||||
|
||||
"github.com/grafana/grafana/pkg/schema/load"
|
||||
"github.com/laher/mergefs"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -35,7 +36,7 @@ func TestValidateScuemataBasics(t *testing.T) {
|
||||
filesystem := fstest.MapFS{
|
||||
"cue/data/gen.cue": &fstest.MapFile{Data: genCue},
|
||||
}
|
||||
mergedFS := Merge(filesystem, defaultBaseLoadPaths.BaseCueFS)
|
||||
mergedFS := mergefs.Merge(filesystem, defaultBaseLoadPaths.BaseCueFS)
|
||||
|
||||
var baseLoadPaths = load.BaseLoadPaths{
|
||||
BaseCueFS: mergedFS,
|
||||
@ -53,7 +54,7 @@ func TestValidateScuemataBasics(t *testing.T) {
|
||||
filesystem := fstest.MapFS{
|
||||
"cue/data/gen.cue": &fstest.MapFile{Data: genCue},
|
||||
}
|
||||
mergedFS := Merge(filesystem, defaultBaseLoadPaths.BaseCueFS)
|
||||
mergedFS := mergefs.Merge(filesystem, defaultBaseLoadPaths.BaseCueFS)
|
||||
|
||||
var baseLoadPaths = load.BaseLoadPaths{
|
||||
BaseCueFS: mergedFS,
|
||||
@ -78,7 +79,7 @@ func TestValidateScuemataBasics(t *testing.T) {
|
||||
"valid.json": &fstest.MapFile{Data: validPanel},
|
||||
"invalid.json": &fstest.MapFile{Data: invalidPanel},
|
||||
}
|
||||
mergedFS := Merge(filesystem, defaultBaseLoadPaths.BaseCueFS)
|
||||
mergedFS := mergefs.Merge(filesystem, defaultBaseLoadPaths.BaseCueFS)
|
||||
|
||||
var baseLoadPaths = load.BaseLoadPaths{
|
||||
BaseCueFS: mergedFS,
|
||||
|
@ -185,6 +185,12 @@ var (
|
||||
grafanaBuildVersion *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() {
|
||||
@ -547,6 +553,18 @@ func init() {
|
||||
Help: "number of evaluation calls",
|
||||
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
|
||||
@ -640,6 +658,8 @@ func initMetricVars() {
|
||||
StatsTotalDashboardVersions,
|
||||
StatsTotalAnnotations,
|
||||
MAccessEvaluationCount,
|
||||
StatsTotalLibraryPanels,
|
||||
StatsTotalLibraryVariables,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -72,6 +72,8 @@ func (uss *UsageStatsService) GetUsageReport(ctx context.Context) (UsageReport,
|
||||
metrics["stats.dashboard_versions.count"] = statsQuery.Result.DashboardVersions
|
||||
metrics["stats.annotations.count"] = statsQuery.Result.Annotations
|
||||
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
|
||||
if uss.License.HasValidLicense() {
|
||||
validLicCount = 1
|
||||
@ -317,6 +319,8 @@ func (uss *UsageStatsService) updateTotalStats() {
|
||||
metrics.StatsTotalDashboardVersions.Set(float64(statsQuery.Result.DashboardVersions))
|
||||
metrics.StatsTotalAnnotations.Set(float64(statsQuery.Result.Annotations))
|
||||
metrics.StatsTotalAlertRules.Set(float64(statsQuery.Result.AlertRules))
|
||||
metrics.StatsTotalLibraryPanels.Set(float64(statsQuery.Result.LibraryPanels))
|
||||
metrics.StatsTotalLibraryVariables.Set(float64(statsQuery.Result.LibraryVariables))
|
||||
|
||||
dsStats := models.GetDataSourceStatsQuery{}
|
||||
if err := uss.Bus.Dispatch(&dsStats); err != nil {
|
||||
|
@ -63,6 +63,8 @@ func TestMetrics(t *testing.T) {
|
||||
DashboardVersions: 16,
|
||||
Annotations: 17,
|
||||
AlertRules: 18,
|
||||
LibraryPanels: 19,
|
||||
LibraryVariables: 20,
|
||||
}
|
||||
getSystemStatsQuery = query
|
||||
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, 17, metrics.Get("stats.annotations.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, 10, metrics.Get("stats.ds."+models.DS_PROMETHEUS+".count").MustInt())
|
||||
|
@ -76,6 +76,7 @@ func (ds *DataSource) HTTPClientOptions() sdkhttpclient.Options {
|
||||
opts := sdkhttpclient.Options{
|
||||
Timeouts: &sdkhttpclient.TimeoutOptions{
|
||||
Timeout: ds.getTimeout(),
|
||||
DialTimeout: time.Duration(setting.DataProxyDialTimeout) * time.Second,
|
||||
KeepAlive: time.Duration(setting.DataProxyKeepAlive) * time.Second,
|
||||
TLSHandshakeTimeout: time.Duration(setting.DataProxyTLSHandshakeTimeout) * time.Second,
|
||||
ExpectContinueTimeout: time.Duration(setting.DataProxyExpectContinueTimeout) * time.Second,
|
||||
|
13
pkg/models/libraryelements.go
Normal file
13
pkg/models/libraryelements.go
Normal 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"
|
@ -19,6 +19,8 @@ type SystemStats struct {
|
||||
DashboardVersions int64
|
||||
Annotations int64
|
||||
AlertRules int64
|
||||
LibraryPanels int64
|
||||
LibraryVariables int64
|
||||
|
||||
Admins int
|
||||
Editors int
|
||||
|
@ -49,7 +49,7 @@ type Manager interface {
|
||||
// ImportDashboard imports a dashboard.
|
||||
ImportDashboard(pluginID, path string, orgID, folderID int64, dashboardModel *simplejson.Json,
|
||||
overwrite bool, inputs []ImportDashboardInput, user *models.SignedInUser,
|
||||
requestHandler DataRequestHandler) (PluginDashboardInfoDTO, error)
|
||||
requestHandler DataRequestHandler) (PluginDashboardInfoDTO, *models.Dashboard, error)
|
||||
// ScanningErrors returns plugin scanning errors encountered.
|
||||
ScanningErrors() []PluginError
|
||||
// LoadPluginDashboard loads a plugin dashboard.
|
||||
|
@ -23,12 +23,12 @@ func (e DashboardInputMissingError) Error() string {
|
||||
|
||||
func (pm *PluginManager) ImportDashboard(pluginID, path string, orgID, folderID int64, dashboardModel *simplejson.Json,
|
||||
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
|
||||
if pluginID != "" {
|
||||
var err error
|
||||
if dashboard, err = pm.LoadPluginDashboard(pluginID, path); err != nil {
|
||||
return plugins.PluginDashboardInfoDTO{}, err
|
||||
return plugins.PluginDashboardInfoDTO{}, &models.Dashboard{}, err
|
||||
}
|
||||
} else {
|
||||
dashboard = models.NewDashboardFromJson(dashboardModel)
|
||||
@ -41,7 +41,7 @@ func (pm *PluginManager) ImportDashboard(pluginID, path string, orgID, folderID
|
||||
|
||||
generatedDash, err := evaluator.Eval()
|
||||
if err != nil {
|
||||
return plugins.PluginDashboardInfoDTO{}, err
|
||||
return plugins.PluginDashboardInfoDTO{}, &models.Dashboard{}, err
|
||||
}
|
||||
|
||||
saveCmd := models.SaveDashboardCommand{
|
||||
@ -62,7 +62,7 @@ func (pm *PluginManager) ImportDashboard(pluginID, path string, orgID, folderID
|
||||
|
||||
savedDash, err := dashboards.NewService(pm.SQLStore).ImportDashboard(dto)
|
||||
if err != nil {
|
||||
return plugins.PluginDashboardInfoDTO{}, err
|
||||
return plugins.PluginDashboardInfoDTO{}, &models.Dashboard{}, err
|
||||
}
|
||||
|
||||
return plugins.PluginDashboardInfoDTO{
|
||||
@ -77,7 +77,7 @@ func (pm *PluginManager) ImportDashboard(pluginID, path string, orgID, folderID
|
||||
Imported: true,
|
||||
DashboardId: savedDash.Id,
|
||||
Slug: savedDash.Slug,
|
||||
}, nil
|
||||
}, savedDash, nil
|
||||
}
|
||||
|
||||
type DashTemplateEvaluator struct {
|
||||
|
@ -21,12 +21,13 @@ func TestDashboardImport(t *testing.T) {
|
||||
mock := &dashboards.FakeDashboardService{}
|
||||
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{
|
||||
{Name: "*", Type: "datasource", Value: "graphite"},
|
||||
}, &models.SignedInUser{UserId: 1, OrgRole: models.ROLE_ADMIN}, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, info)
|
||||
require.NotNil(t, dash)
|
||||
|
||||
resultStr, err := mock.SavedDashboards[0].Dashboard.Data.EncodePretty()
|
||||
require.NoError(t, err)
|
||||
|
@ -60,30 +60,21 @@ func (e *BadRequestError) Error() string {
|
||||
type ErrVersionUnsupported struct {
|
||||
PluginID string
|
||||
RequestedVersion string
|
||||
RecommendedVersion string
|
||||
SystemInfo string
|
||||
}
|
||||
|
||||
func (e ErrVersionUnsupported) Error() string {
|
||||
if len(e.RecommendedVersion) > 0 {
|
||||
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)
|
||||
return fmt.Sprintf("%s v%s is not supported on your system (%s)", e.PluginID, e.RequestedVersion, e.SystemInfo)
|
||||
}
|
||||
|
||||
type ErrVersionNotFound struct {
|
||||
PluginID string
|
||||
RequestedVersion string
|
||||
RecommendedVersion string
|
||||
SystemInfo string
|
||||
}
|
||||
|
||||
func (e ErrVersionNotFound) Error() string {
|
||||
if len(e.RecommendedVersion) > 0 {
|
||||
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)
|
||||
return fmt.Sprintf("%s v%s either does not exist or is not supported on your system (%s)", e.PluginID, e.RequestedVersion, e.SystemInfo)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
v, err := selectVersion(&plugin, version)
|
||||
v, err := i.selectVersion(&plugin, version)
|
||||
if err != nil {
|
||||
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
|
||||
// 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.
|
||||
func selectVersion(plugin *Plugin, version string) (*Version, error) {
|
||||
func (i *Installer) selectVersion(plugin *Plugin, version string) (*Version, error) {
|
||||
var ver Version
|
||||
|
||||
latestForArch := latestSupportedVersion(plugin)
|
||||
@ -437,6 +428,7 @@ func selectVersion(plugin *Plugin, version string) (*Version, error) {
|
||||
return nil, ErrVersionUnsupported{
|
||||
PluginID: plugin.ID,
|
||||
RequestedVersion: version,
|
||||
SystemInfo: i.fullSystemInfoString(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -451,24 +443,32 @@ func selectVersion(plugin *Plugin, version string) (*Version, error) {
|
||||
}
|
||||
|
||||
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{
|
||||
PluginID: plugin.ID,
|
||||
RequestedVersion: version,
|
||||
RecommendedVersion: latestForArch.Version,
|
||||
SystemInfo: i.fullSystemInfoString(),
|
||||
}
|
||||
}
|
||||
|
||||
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{
|
||||
PluginID: plugin.ID,
|
||||
RequestedVersion: version,
|
||||
RecommendedVersion: latestForArch.Version,
|
||||
SystemInfo: i.fullSystemInfoString(),
|
||||
}
|
||||
}
|
||||
|
||||
return &ver, nil
|
||||
}
|
||||
|
||||
func (i *Installer) fullSystemInfoString() string {
|
||||
return fmt.Sprintf("Grafana v%s %s", i.grafanaVersion, osAndArchString())
|
||||
}
|
||||
|
||||
func osAndArchString() string {
|
||||
osString := strings.ToLower(runtime.GOOS)
|
||||
arch := runtime.GOARCH
|
||||
|
@ -145,7 +145,7 @@ func (s *Service) autoUpdateAppDashboard(pluginDashInfo *plugins.PluginDashboard
|
||||
s.logger.Info("Auto updating App dashboard", "dashboard", dash.Title, "newRev",
|
||||
pluginDashInfo.Revision, "oldRev", pluginDashInfo.ImportedRevision)
|
||||
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)
|
||||
return err
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ SELECT DISTINCT
|
||||
, u1.email AS created_by_email
|
||||
, u2.login AS updated_by_name
|
||||
, 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 {
|
||||
@ -41,9 +41,9 @@ func syncFieldsWithModel(libraryElement *LibraryElement) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if LibraryElementKind(libraryElement.Kind) == Panel {
|
||||
if models.LibraryElementKind(libraryElement.Kind) == models.PanelElement {
|
||||
model["title"] = libraryElement.Name
|
||||
} else if LibraryElementKind(libraryElement.Kind) == Variable {
|
||||
} else if models.LibraryElementKind(libraryElement.Kind) == models.VariableElement {
|
||||
model["name"] = libraryElement.Name
|
||||
}
|
||||
if model["type"] != nil {
|
||||
@ -520,7 +520,7 @@ func (l *LibraryElementService) getConnections(c *models.ReqContext, uid string)
|
||||
var libraryElementConnections []libraryElementConnectionWithMeta
|
||||
builder := sqlstore.SQLBuilder{}
|
||||
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(" INNER JOIN dashboard AS dashboard on lec.connection_id = dashboard.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" +
|
||||
getFromLibraryElementDTOWithMeta(l.SQLStore.Dialect) +
|
||||
" 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)
|
||||
err := sess.Find(&libraryElements)
|
||||
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.
|
||||
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 := 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 {
|
||||
return err
|
||||
}
|
||||
@ -646,7 +646,7 @@ func (l *LibraryElementService) connectElementsToDashboardID(c *models.ReqContex
|
||||
// disconnectElementsFromDashboardID deletes connections for all Library Elements in a Dashboard.
|
||||
func (l *LibraryElementService) disconnectElementsFromDashboardID(c *models.ReqContext, dashboardID int64) 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 {
|
||||
return err
|
||||
}
|
||||
@ -676,7 +676,7 @@ func (l *LibraryElementService) deleteLibraryElementsInFolderUID(c *models.ReqCo
|
||||
ConnectionID int64 `xorm:"connection_id"`
|
||||
}
|
||||
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=?"
|
||||
err = session.SQL(sql, folderID, c.SignedInUser.OrgId).Find(&connectionIDs)
|
||||
if err != nil {
|
||||
@ -694,7 +694,7 @@ func (l *LibraryElementService) deleteLibraryElementsInFolderUID(c *models.ReqCo
|
||||
return err
|
||||
}
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
@ -11,11 +11,11 @@ func isGeneralFolder(folderID int64) bool {
|
||||
}
|
||||
|
||||
func (l *LibraryElementService) requireSupportedElementKind(kindAsInt int64) error {
|
||||
kind := LibraryElementKind(kindAsInt)
|
||||
kind := models.LibraryElementKind(kindAsInt)
|
||||
switch kind {
|
||||
case Panel:
|
||||
case models.PanelElement:
|
||||
return nil
|
||||
case Variable:
|
||||
case models.VariableElement:
|
||||
return nil
|
||||
default:
|
||||
return errLibraryElementUnSupportedElementKind
|
||||
|
@ -6,7 +6,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
@ -27,8 +26,6 @@ type LibraryElementService struct {
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
const connectionTableName = "library_element_connection"
|
||||
|
||||
func init() {
|
||||
registry.RegisterService(&LibraryElementService{})
|
||||
}
|
||||
@ -66,51 +63,3 @@ func (l *LibraryElementService) DisconnectElementsFromDashboard(c *models.ReqCon
|
||||
func (l *LibraryElementService) DeleteLibraryElementsInFolder(c *models.ReqContext, folderUID string) error {
|
||||
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]))
|
||||
}
|
||||
|
@ -5,6 +5,8 @@ import (
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
func TestCreateLibraryElement(t *testing.T) {
|
||||
@ -24,7 +26,7 @@ func TestCreateLibraryElement(t *testing.T) {
|
||||
FolderID: 1,
|
||||
UID: sc.initialResult.Result.UID,
|
||||
Name: "Text - Library Panel",
|
||||
Kind: int64(Panel),
|
||||
Kind: int64(models.PanelElement),
|
||||
Type: "text",
|
||||
Description: "A description",
|
||||
Model: map[string]interface{}{
|
||||
@ -69,7 +71,7 @@ func TestCreateLibraryElement(t *testing.T) {
|
||||
FolderID: 1,
|
||||
UID: result.Result.UID,
|
||||
Name: "Library Panel Name",
|
||||
Kind: int64(Panel),
|
||||
Kind: int64(models.PanelElement),
|
||||
Type: "text",
|
||||
Description: "A description",
|
||||
Model: map[string]interface{}{
|
||||
|
@ -42,7 +42,7 @@ func TestGetAllLibraryElements(t *testing.T) {
|
||||
|
||||
err := sc.reqContext.Req.ParseForm()
|
||||
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)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
@ -62,7 +62,7 @@ func TestGetAllLibraryElements(t *testing.T) {
|
||||
FolderID: 1,
|
||||
UID: result.Result.Elements[0].UID,
|
||||
Name: "Text - Library Panel",
|
||||
Kind: int64(Panel),
|
||||
Kind: int64(models.PanelElement),
|
||||
Type: "text",
|
||||
Description: "A description",
|
||||
Model: map[string]interface{}{
|
||||
@ -107,7 +107,7 @@ func TestGetAllLibraryElements(t *testing.T) {
|
||||
|
||||
err := sc.reqContext.Req.ParseForm()
|
||||
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)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
@ -127,7 +127,7 @@ func TestGetAllLibraryElements(t *testing.T) {
|
||||
FolderID: 1,
|
||||
UID: result.Result.Elements[0].UID,
|
||||
Name: "query0",
|
||||
Kind: int64(Variable),
|
||||
Kind: int64(models.VariableElement),
|
||||
Type: "query",
|
||||
Description: "A description",
|
||||
Model: map[string]interface{}{
|
||||
@ -187,7 +187,7 @@ func TestGetAllLibraryElements(t *testing.T) {
|
||||
FolderID: 1,
|
||||
UID: result.Result.Elements[0].UID,
|
||||
Name: "Text - Library Panel",
|
||||
Kind: int64(Panel),
|
||||
Kind: int64(models.PanelElement),
|
||||
Type: "text",
|
||||
Description: "A description",
|
||||
Model: map[string]interface{}{
|
||||
@ -222,7 +222,7 @@ func TestGetAllLibraryElements(t *testing.T) {
|
||||
FolderID: 1,
|
||||
UID: result.Result.Elements[1].UID,
|
||||
Name: "Text - Library Panel2",
|
||||
Kind: int64(Panel),
|
||||
Kind: int64(models.PanelElement),
|
||||
Type: "text",
|
||||
Description: "A description",
|
||||
Model: map[string]interface{}{
|
||||
@ -286,7 +286,7 @@ func TestGetAllLibraryElements(t *testing.T) {
|
||||
FolderID: 1,
|
||||
UID: result.Result.Elements[0].UID,
|
||||
Name: "Text - Library Panel2",
|
||||
Kind: int64(Panel),
|
||||
Kind: int64(models.PanelElement),
|
||||
Type: "text",
|
||||
Description: "A description",
|
||||
Model: map[string]interface{}{
|
||||
@ -321,7 +321,7 @@ func TestGetAllLibraryElements(t *testing.T) {
|
||||
FolderID: 1,
|
||||
UID: result.Result.Elements[1].UID,
|
||||
Name: "Text - Library Panel",
|
||||
Kind: int64(Panel),
|
||||
Kind: int64(models.PanelElement),
|
||||
Type: "text",
|
||||
Description: "A description",
|
||||
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",
|
||||
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}",
|
||||
"id": 1,
|
||||
@ -372,7 +372,7 @@ func TestGetAllLibraryElements(t *testing.T) {
|
||||
resp := sc.service.createHandler(sc.reqContext, command)
|
||||
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}",
|
||||
"id": 1,
|
||||
@ -405,7 +405,7 @@ func TestGetAllLibraryElements(t *testing.T) {
|
||||
FolderID: 1,
|
||||
UID: result.Result.Elements[0].UID,
|
||||
Name: "BarGauge - Library Panel",
|
||||
Kind: int64(Panel),
|
||||
Kind: int64(models.PanelElement),
|
||||
Type: "bargauge",
|
||||
Description: "BarGauge description",
|
||||
Model: map[string]interface{}{
|
||||
@ -440,7 +440,7 @@ func TestGetAllLibraryElements(t *testing.T) {
|
||||
FolderID: 1,
|
||||
UID: result.Result.Elements[1].UID,
|
||||
Name: "Gauge - Library Panel",
|
||||
Kind: int64(Panel),
|
||||
Kind: int64(models.PanelElement),
|
||||
Type: "gauge",
|
||||
Description: "Gauge description",
|
||||
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",
|
||||
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}",
|
||||
"id": 1,
|
||||
@ -542,7 +542,7 @@ func TestGetAllLibraryElements(t *testing.T) {
|
||||
FolderID: newFolder.Id,
|
||||
UID: result.Result.Elements[0].UID,
|
||||
Name: "Text - Library Panel2",
|
||||
Kind: int64(Panel),
|
||||
Kind: int64(models.PanelElement),
|
||||
Type: "text",
|
||||
Description: "A description",
|
||||
Model: map[string]interface{}{
|
||||
@ -637,7 +637,7 @@ func TestGetAllLibraryElements(t *testing.T) {
|
||||
FolderID: 1,
|
||||
UID: result.Result.Elements[0].UID,
|
||||
Name: "Text - Library Panel",
|
||||
Kind: int64(Panel),
|
||||
Kind: int64(models.PanelElement),
|
||||
Type: "text",
|
||||
Description: "A description",
|
||||
Model: map[string]interface{}{
|
||||
@ -672,7 +672,7 @@ func TestGetAllLibraryElements(t *testing.T) {
|
||||
FolderID: 1,
|
||||
UID: result.Result.Elements[1].UID,
|
||||
Name: "Text - Library Panel2",
|
||||
Kind: int64(Panel),
|
||||
Kind: int64(models.PanelElement),
|
||||
Type: "text",
|
||||
Description: "A description",
|
||||
Model: map[string]interface{}{
|
||||
@ -736,7 +736,7 @@ func TestGetAllLibraryElements(t *testing.T) {
|
||||
FolderID: 1,
|
||||
UID: result.Result.Elements[0].UID,
|
||||
Name: "Text - Library Panel2",
|
||||
Kind: int64(Panel),
|
||||
Kind: int64(models.PanelElement),
|
||||
Type: "text",
|
||||
Description: "A description",
|
||||
Model: map[string]interface{}{
|
||||
@ -800,7 +800,7 @@ func TestGetAllLibraryElements(t *testing.T) {
|
||||
FolderID: 1,
|
||||
UID: result.Result.Elements[0].UID,
|
||||
Name: "Text - Library Panel",
|
||||
Kind: int64(Panel),
|
||||
Kind: int64(models.PanelElement),
|
||||
Type: "text",
|
||||
Description: "A description",
|
||||
Model: map[string]interface{}{
|
||||
@ -865,7 +865,7 @@ func TestGetAllLibraryElements(t *testing.T) {
|
||||
FolderID: 1,
|
||||
UID: result.Result.Elements[0].UID,
|
||||
Name: "Text - Library Panel2",
|
||||
Kind: int64(Panel),
|
||||
Kind: int64(models.PanelElement),
|
||||
Type: "text",
|
||||
Description: "A description",
|
||||
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",
|
||||
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}",
|
||||
"id": 1,
|
||||
@ -939,7 +939,7 @@ func TestGetAllLibraryElements(t *testing.T) {
|
||||
FolderID: 1,
|
||||
UID: result.Result.Elements[0].UID,
|
||||
Name: "Text - Library Panel",
|
||||
Kind: int64(Panel),
|
||||
Kind: int64(models.PanelElement),
|
||||
Type: "text",
|
||||
Description: "A description",
|
||||
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",
|
||||
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}",
|
||||
"id": 1,
|
||||
@ -1011,7 +1011,7 @@ func TestGetAllLibraryElements(t *testing.T) {
|
||||
FolderID: 1,
|
||||
UID: result.Result.Elements[0].UID,
|
||||
Name: "Some Other",
|
||||
Kind: int64(Panel),
|
||||
Kind: int64(models.PanelElement),
|
||||
Type: "text",
|
||||
Description: "A Library Panel",
|
||||
Model: map[string]interface{}{
|
||||
@ -1046,7 +1046,7 @@ func TestGetAllLibraryElements(t *testing.T) {
|
||||
FolderID: 1,
|
||||
UID: result.Result.Elements[1].UID,
|
||||
Name: "Text - Library Panel",
|
||||
Kind: int64(Panel),
|
||||
Kind: int64(models.PanelElement),
|
||||
Type: "text",
|
||||
Description: "A description",
|
||||
Model: map[string]interface{}{
|
||||
@ -1112,7 +1112,7 @@ func TestGetAllLibraryElements(t *testing.T) {
|
||||
FolderID: 1,
|
||||
UID: result.Result.Elements[0].UID,
|
||||
Name: "Text - Library Panel2",
|
||||
Kind: int64(Panel),
|
||||
Kind: int64(models.PanelElement),
|
||||
Type: "text",
|
||||
Description: "A description",
|
||||
Model: map[string]interface{}{
|
||||
|
@ -35,7 +35,7 @@ func TestGetLibraryElement(t *testing.T) {
|
||||
FolderID: 1,
|
||||
UID: res.Result.UID,
|
||||
Name: "Text - Library Panel",
|
||||
Kind: int64(Panel),
|
||||
Kind: int64(models.PanelElement),
|
||||
Type: "text",
|
||||
Description: "A description",
|
||||
Model: map[string]interface{}{
|
||||
@ -130,7 +130,7 @@ func TestGetLibraryElement(t *testing.T) {
|
||||
FolderID: 1,
|
||||
UID: res.Result.UID,
|
||||
Name: "Text - Library Panel",
|
||||
Kind: int64(Panel),
|
||||
Kind: int64(models.PanelElement),
|
||||
Type: "text",
|
||||
Description: "A description",
|
||||
Model: map[string]interface{}{
|
||||
|
@ -5,12 +5,14 @@ import (
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
func TestPatchLibraryElement(t *testing.T) {
|
||||
scenarioWithPanel(t, "When an admin tries to patch a library panel that does not exist, it should fail",
|
||||
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"})
|
||||
resp := sc.service.patchHandler(sc.reqContext, cmd)
|
||||
require.Equal(t, 404, resp.Status())
|
||||
@ -31,7 +33,7 @@ func TestPatchLibraryElement(t *testing.T) {
|
||||
"type": "graph"
|
||||
}
|
||||
`),
|
||||
Kind: int64(Panel),
|
||||
Kind: int64(models.PanelElement),
|
||||
Version: 1,
|
||||
}
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
|
||||
@ -45,7 +47,7 @@ func TestPatchLibraryElement(t *testing.T) {
|
||||
FolderID: newFolder.Id,
|
||||
UID: sc.initialResult.Result.UID,
|
||||
Name: "Panel - New name",
|
||||
Kind: int64(Panel),
|
||||
Kind: int64(models.PanelElement),
|
||||
Type: "graph",
|
||||
Description: "An updated description",
|
||||
Model: map[string]interface{}{
|
||||
@ -83,7 +85,7 @@ func TestPatchLibraryElement(t *testing.T) {
|
||||
newFolder := createFolderWithACL(t, sc.sqlStore, "NewFolder", sc.user, []folderACLItem{})
|
||||
cmd := patchLibraryElementCommand{
|
||||
FolderID: newFolder.Id,
|
||||
Kind: int64(Panel),
|
||||
Kind: int64(models.PanelElement),
|
||||
Version: 1,
|
||||
}
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
|
||||
@ -104,7 +106,7 @@ func TestPatchLibraryElement(t *testing.T) {
|
||||
cmd := patchLibraryElementCommand{
|
||||
FolderID: -1,
|
||||
Name: "New Name",
|
||||
Kind: int64(Panel),
|
||||
Kind: int64(models.PanelElement),
|
||||
Version: 1,
|
||||
}
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
|
||||
@ -125,7 +127,7 @@ func TestPatchLibraryElement(t *testing.T) {
|
||||
cmd := patchLibraryElementCommand{
|
||||
FolderID: -1,
|
||||
Model: []byte(`{ "title": "New Model Title", "name": "New Model Name", "type":"graph", "description": "New description" }`),
|
||||
Kind: int64(Panel),
|
||||
Kind: int64(models.PanelElement),
|
||||
Version: 1,
|
||||
}
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
|
||||
@ -152,7 +154,7 @@ func TestPatchLibraryElement(t *testing.T) {
|
||||
cmd := patchLibraryElementCommand{
|
||||
FolderID: -1,
|
||||
Model: []byte(`{ "description": "New description" }`),
|
||||
Kind: int64(Panel),
|
||||
Kind: int64(models.PanelElement),
|
||||
Version: 1,
|
||||
}
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
|
||||
@ -178,7 +180,7 @@ func TestPatchLibraryElement(t *testing.T) {
|
||||
cmd := patchLibraryElementCommand{
|
||||
FolderID: -1,
|
||||
Model: []byte(`{ "type": "graph" }`),
|
||||
Kind: int64(Panel),
|
||||
Kind: int64(models.PanelElement),
|
||||
Version: 1,
|
||||
}
|
||||
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",
|
||||
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.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
|
||||
resp := sc.service.patchHandler(sc.reqContext, cmd)
|
||||
@ -223,7 +225,7 @@ func TestPatchLibraryElement(t *testing.T) {
|
||||
cmd := patchLibraryElementCommand{
|
||||
Name: "Text - Library Panel",
|
||||
Version: 1,
|
||||
Kind: int64(Panel),
|
||||
Kind: int64(models.PanelElement),
|
||||
}
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID})
|
||||
resp = sc.service.patchHandler(sc.reqContext, cmd)
|
||||
@ -239,7 +241,7 @@ func TestPatchLibraryElement(t *testing.T) {
|
||||
cmd := patchLibraryElementCommand{
|
||||
FolderID: 1,
|
||||
Version: 1,
|
||||
Kind: int64(Panel),
|
||||
Kind: int64(models.PanelElement),
|
||||
}
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID})
|
||||
resp = sc.service.patchHandler(sc.reqContext, cmd)
|
||||
@ -251,7 +253,7 @@ func TestPatchLibraryElement(t *testing.T) {
|
||||
cmd := patchLibraryElementCommand{
|
||||
FolderID: sc.folder.Id,
|
||||
Version: 1,
|
||||
Kind: int64(Panel),
|
||||
Kind: int64(models.PanelElement),
|
||||
}
|
||||
sc.reqContext.OrgId = 2
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
|
||||
@ -264,7 +266,7 @@ func TestPatchLibraryElement(t *testing.T) {
|
||||
cmd := patchLibraryElementCommand{
|
||||
FolderID: sc.folder.Id,
|
||||
Version: 1,
|
||||
Kind: int64(Panel),
|
||||
Kind: int64(models.PanelElement),
|
||||
}
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
|
||||
resp := sc.service.patchHandler(sc.reqContext, cmd)
|
||||
@ -278,14 +280,14 @@ func TestPatchLibraryElement(t *testing.T) {
|
||||
cmd := patchLibraryElementCommand{
|
||||
FolderID: sc.folder.Id,
|
||||
Version: 1,
|
||||
Kind: int64(Variable),
|
||||
Kind: int64(models.VariableElement),
|
||||
}
|
||||
sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID})
|
||||
resp := sc.service.patchHandler(sc.reqContext, cmd)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
var result = validateAndUnMarshalResponse(t, resp)
|
||||
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.Model = map[string]interface{}{
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
|
@ -84,7 +84,7 @@ func TestLibraryElementPermissions(t *testing.T) {
|
||||
toFolder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, testCase.items)
|
||||
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})
|
||||
resp = sc.service.patchHandler(sc.reqContext, cmd)
|
||||
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)
|
||||
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})
|
||||
resp = sc.service.patchHandler(sc.reqContext, cmd)
|
||||
require.Equal(t, testCase.status, resp.Status())
|
||||
@ -146,7 +146,7 @@ func TestLibraryElementPermissions(t *testing.T) {
|
||||
result := validateAndUnMarshalResponse(t, resp)
|
||||
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})
|
||||
resp = sc.service.patchHandler(sc.reqContext, cmd)
|
||||
require.Equal(t, testCase.status, resp.Status())
|
||||
@ -160,7 +160,7 @@ func TestLibraryElementPermissions(t *testing.T) {
|
||||
result := validateAndUnMarshalResponse(t, resp)
|
||||
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})
|
||||
resp = sc.service.patchHandler(sc.reqContext, cmd)
|
||||
require.Equal(t, testCase.status, resp.Status())
|
||||
@ -205,7 +205,7 @@ func TestLibraryElementPermissions(t *testing.T) {
|
||||
result := validateAndUnMarshalResponse(t, resp)
|
||||
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})
|
||||
resp = sc.service.patchHandler(sc.reqContext, cmd)
|
||||
require.Equal(t, 404, resp.Status())
|
||||
|
@ -126,7 +126,7 @@ type libraryElementsSearchResult struct {
|
||||
}
|
||||
|
||||
func getCreatePanelCommand(folderID int64, name string) CreateLibraryElementCommand {
|
||||
command := getCreateCommandWithModel(folderID, name, Panel, []byte(`
|
||||
command := getCreateCommandWithModel(folderID, name, models.PanelElement, []byte(`
|
||||
{
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"id": 1,
|
||||
@ -140,7 +140,7 @@ func getCreatePanelCommand(folderID int64, name string) CreateLibraryElementComm
|
||||
}
|
||||
|
||||
func getCreateVariableCommand(folderID int64, name string) CreateLibraryElementCommand {
|
||||
command := getCreateCommandWithModel(folderID, name, Variable, []byte(`
|
||||
command := getCreateCommandWithModel(folderID, name, models.VariableElement, []byte(`
|
||||
{
|
||||
"datasource": "${DS_GDEV-TESTDATA}",
|
||||
"name": "query0",
|
||||
@ -152,7 +152,7 @@ func getCreateVariableCommand(folderID int64, name string) CreateLibraryElementC
|
||||
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{
|
||||
FolderID: folderID,
|
||||
Name: name,
|
||||
|
@ -6,13 +6,6 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type LibraryElementKind int
|
||||
|
||||
const (
|
||||
Panel LibraryElementKind = iota + 1
|
||||
Variable
|
||||
)
|
||||
|
||||
type LibraryConnectionKind int
|
||||
|
||||
const (
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"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) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,6 @@ type Service interface {
|
||||
LoadLibraryPanelsForDashboard(c *models.ReqContext, dash *models.Dashboard) error
|
||||
CleanLibraryPanelsForDashboard(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.
|
||||
@ -76,7 +75,7 @@ func (lps *LibraryPanelService) LoadLibraryPanelsForDashboard(c *models.ReqConte
|
||||
continue
|
||||
}
|
||||
|
||||
if libraryelements.LibraryElementKind(elementInDB.Kind) != libraryelements.Panel {
|
||||
if models.LibraryElementKind(elementInDB.Kind) != models.PanelElement {
|
||||
continue
|
||||
}
|
||||
|
||||
@ -193,11 +192,3 @@ func (lps *LibraryPanelService) ConnectLibraryPanelsForDashboard(c *models.ReqCo
|
||||
|
||||
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)
|
||||
}
|
||||
|
@ -493,7 +493,7 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) {
|
||||
"description": "Unused description"
|
||||
}
|
||||
`),
|
||||
Kind: int64(libraryelements.Panel),
|
||||
Kind: int64(models.PanelElement),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
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 {
|
||||
ID int64
|
||||
OrgID int64
|
||||
@ -783,7 +683,7 @@ func scenarioWithLibraryPanel(t *testing.T, desc string, fn func(t *testing.T, s
|
||||
"description": "A description"
|
||||
}
|
||||
`),
|
||||
Kind: int64(libraryelements.Panel),
|
||||
Kind: int64(models.PanelElement),
|
||||
}
|
||||
resp, err := sc.elementService.CreateElement(sc.reqContext, command)
|
||||
require.NoError(t, err)
|
||||
|
@ -69,6 +69,7 @@ func (ng *AlertNG) Init() error {
|
||||
BaseInterval: baseInterval,
|
||||
DefaultIntervalSeconds: defaultIntervalSeconds,
|
||||
SQLStore: ng.SQLStore,
|
||||
Logger: ng.Log,
|
||||
}
|
||||
|
||||
var err error
|
||||
|
@ -442,6 +442,8 @@ func (am *Alertmanager) buildReceiverIntegrations(receiver *apimodels.PostableAp
|
||||
n, err = channels.NewThreemaNotifier(cfg, tmpl)
|
||||
case "opsgenie":
|
||||
n, err = channels.NewOpsgenieNotifier(cfg, tmpl)
|
||||
case "prometheus-alertmanager":
|
||||
n, err = channels.NewAlertmanagerNotifier(cfg, tmpl)
|
||||
default:
|
||||
return nil, fmt.Errorf("notifier %s is not supported", r.Type)
|
||||
}
|
||||
|
@ -9,6 +9,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
|
||||
gokit_log "github.com/go-kit/kit/log"
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/prometheus/alertmanager/api/v2/models"
|
||||
@ -42,6 +44,7 @@ func setupAMTest(t *testing.T) *Alertmanager {
|
||||
BaseInterval: 10 * time.Second,
|
||||
DefaultIntervalSeconds: 60,
|
||||
SQLStore: sqlStore,
|
||||
Logger: log.New("alertmanager-test"),
|
||||
}
|
||||
|
||||
am, err := New(cfg, store, m)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user