mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into panel-edit-ux
This commit is contained in:
commit
964a21b091
@ -344,6 +344,7 @@ header_property = username
|
||||
auto_sign_up = true
|
||||
ldap_sync_ttl = 60
|
||||
whitelist =
|
||||
headers =
|
||||
|
||||
#################################### Auth LDAP ###########################
|
||||
[auth.ldap]
|
||||
|
@ -294,6 +294,7 @@ log_queries =
|
||||
;auto_sign_up = true
|
||||
;ldap_sync_ttl = 60
|
||||
;whitelist = 192.168.1.1, 192.168.2.1
|
||||
;headers = Email:X-User-Email, Name:X-User-Name
|
||||
|
||||
#################################### Basic Auth ##########################
|
||||
[auth.basic]
|
||||
|
@ -1,116 +0,0 @@
|
||||
+++
|
||||
title = "Permissions"
|
||||
description = "Grafana user permissions"
|
||||
keywords = ["grafana", "configuration", "documentation", "admin", "users", "permissions"]
|
||||
type = "docs"
|
||||
aliases = ["/reference/admin"]
|
||||
[menu.docs]
|
||||
name = "Permissions"
|
||||
parent = "admin"
|
||||
weight = 3
|
||||
+++
|
||||
|
||||
# Permissions
|
||||
|
||||
Grafana users have permissions that are determined by their:
|
||||
|
||||
- **Organization Role** (Admin, Editor, Viewer)
|
||||
- Via **Team** memberships where the **Team** has been assigned specific permissions.
|
||||
- Via permissions assigned directly to user (on folders or dashboards)
|
||||
- The Grafana Admin (i.e. Super Admin) user flag.
|
||||
|
||||
## Organization Roles
|
||||
|
||||
Users can be 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.
|
||||
|
||||
### Admin Role
|
||||
|
||||
Can do everything scoped to the organization. For example:
|
||||
|
||||
- Add & Edit data sources.
|
||||
- Add & Edit organization users & teams.
|
||||
- Configure App plugins & set org settings.
|
||||
|
||||
### Editor Role
|
||||
|
||||
- Can create and modify dashboards & alert rules. This can be disabled on specific folders and dashboards.
|
||||
- **Cannot** create or edit data sources nor invite new users.
|
||||
|
||||
### Viewer Role
|
||||
|
||||
- View any dashboard. This can be disabled on specific folders and dashboards.
|
||||
- **Cannot** create or edit dashboards nor data sources.
|
||||
|
||||
This role can be tweaked via Grafana server setting [viewers_can_edit]({{< relref "installation/configuration.md#viewers-can-edit" >}}). If you set this to true users
|
||||
with **Viewer** can also make transient dashboard edits, meaning they can modify panels & queries but not save the changes (nor create new dashboards).
|
||||
Useful for public Grafana installations where you want anonymous users to be able to edit panels & queries but not save or create new dashboards.
|
||||
|
||||
## Grafana Admin
|
||||
|
||||
This admin flag makes a user a `Super Admin`. This means they can access the `Server Admin` views where all users and organizations can be administrated.
|
||||
|
||||
### Dashboard & Folder Permissions
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v50/folder_permissions.png" max-width="500px" class="docs-image--right" >}}
|
||||
|
||||
For dashboards and dashboard folders there is a **Permissions** page that make it possible to
|
||||
remove the default role based permissions for Editors and Viewers. It's here you can add and assign permissions to specific **Users** and **Teams**.
|
||||
|
||||
You can assign & remove permissions for **Organization Roles**, **Users** and **Teams**.
|
||||
|
||||
Permission levels:
|
||||
|
||||
- **Admin**: Can edit & create dashboards and edit permissions.
|
||||
- **Edit**: Can edit & create dashboards. **Cannot** edit folder/dashboard permissions.
|
||||
- **View**: Can only view existing dashboards/folders.
|
||||
|
||||
#### Restricting 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 **Org Admin Role**. Admins always have access to everything.
|
||||
- A more specific permission with a lower permission level will not have any effect if a more general rule exists with higher permission level. You need to remove or lower the permission level of the more general rule.
|
||||
|
||||
#### How Grafana Resolves Multiple Permissions - Examples
|
||||
|
||||
##### Example 1 (`user1` has the Editor Role)
|
||||
|
||||
Permissions for a dashboard:
|
||||
|
||||
- `Everyone with Editor Role Can Edit`
|
||||
- `user1 Can View`
|
||||
|
||||
Result: `user1` has Edit permission as the highest permission always wins.
|
||||
|
||||
##### Example 2 (`user1` has the Viewer Role and is a member of `team1`)
|
||||
|
||||
Permissions for a dashboard:
|
||||
|
||||
- `Everyone with Viewer Role Can View`
|
||||
- `user1 Can Edit`
|
||||
- `team1 Can Admin`
|
||||
|
||||
Result: `user1` has Admin permission as the highest permission always wins.
|
||||
|
||||
##### Example 3
|
||||
|
||||
Permissions for a dashboard:
|
||||
|
||||
- `user1 Can Admin (inherited from parent folder)`
|
||||
- `user1 Can Edit`
|
||||
|
||||
Result: You cannot override to a lower permission. `user1` has Admin permission as the highest permission always wins.
|
||||
|
||||
- **View**: Can only view existing dashboards/folders.
|
||||
- You cannot override permissions for users with **Org Admin Role**
|
||||
- A more specific permission with lower permission level will not have any effect if a more general rule exists with higher permission level. For example if "Everyone with Editor Role Can Edit" exists in the ACL list then **John Doe** will still have Edit permission even after you have specifically added a permission for this user with the permission set to **View**. You need to remove or lower the permission level of the more general rule.
|
||||
|
||||
### Data source permissions
|
||||
|
||||
Permissions on dashboards and folders **do not** include permissions on data sources. A user with `Viewer` role
|
||||
can still issue any possible query to a data source, not just those queries that exist on dashboards he/she has access to.
|
||||
We hope to add permissions on data sources in a future release. Until then **do not** view dashboard permissions as a secure
|
||||
way to restrict user data access. Dashboard permissions only limits what dashboards & folders a user can view & edit not which
|
||||
data sources a user can access nor what queries a user can issue.
|
||||
|
@ -156,7 +156,7 @@ Since not all datasources have the same configuration settings we only have the
|
||||
| tlsSkipVerify | boolean | *All* | Controls whether a client verifies the server's certificate chain and host name. |
|
||||
| graphiteVersion | string | Graphite | Graphite version |
|
||||
| timeInterval | string | Prometheus, Elasticsearch, InfluxDB, MySQL, PostgreSQL & MSSQL | Lowest interval/step value that should be used for this data source |
|
||||
| esVersion | number | Elasticsearch | Elasticsearch version as a number (2/5/56) |
|
||||
| esVersion | number | Elasticsearch | Elasticsearch version as a number (2/5/56/60) |
|
||||
| timeField | string | Elasticsearch | Which field that should be used as timestamp |
|
||||
| interval | string | Elasticsearch | Index date time format. nil(No Pattern), 'Hourly', 'Daily', 'Weekly', 'Monthly' or 'Yearly' |
|
||||
| authType | string | Cloudwatch | Auth provider. keys/credentials/arn |
|
||||
|
43
docs/sources/auth/enhanced_ldap.md
Normal file
43
docs/sources/auth/enhanced_ldap.md
Normal file
@ -0,0 +1,43 @@
|
||||
+++
|
||||
title = "Enhanced LDAP Integration"
|
||||
description = "Grafana Enhanced LDAP Integration Guide "
|
||||
keywords = ["grafana", "configuration", "documentation", "ldap", "active directory", "enterprise"]
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "Enhanced LDAP"
|
||||
identifier = "enhanced-ldap"
|
||||
parent = "authentication"
|
||||
weight = 3
|
||||
+++
|
||||
|
||||
# Enhanced LDAP Integration
|
||||
|
||||
> Enhanced LDAP Integration is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "enterprise/index.md" >}}).
|
||||
|
||||
The enhanced LDAP integration adds additional functionality on top of the [existing LDAP integration]({{< relref "auth/ldap.md" >}}).
|
||||
|
||||
## LDAP Group Synchronization for Teams
|
||||
|
||||
{{< docs-imagebox img="/img/docs/enterprise/team_members_ldap.png" class="docs-image--no-shadow docs-image--right" max-width= "600px" >}}
|
||||
|
||||
With the enhanced LDAP integration it's possible to setup synchronization between LDAP groups and teams. This enables LDAP users which are members
|
||||
of certain LDAP groups to automatically be added/removed as members to certain teams in Grafana. Currently the synchronization will only happen every
|
||||
time a user logs in, but an active background synchronization is currently being developed.
|
||||
|
||||
Grafana keeps track of all synchronized users in teams and you can see which users have been synchronized from LDAP in the team members list, see `LDAP` label in screenshot.
|
||||
This mechanism allows Grafana to remove an existing synchronized user from a team when its LDAP group membership changes. This mechanism also enables you to manually add
|
||||
a user as member of a team and it will not be removed when the user signs in. This gives you flexibility to combine LDAP group memberships and Grafana team memberships.
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
### Enable LDAP group synchronization for a team
|
||||
|
||||
{{< docs-imagebox img="/img/docs/enterprise/team_add_external_group.png" class="docs-image--no-shadow docs-image--right" max-width= "600px" >}}
|
||||
|
||||
1. Navigate to Configuration / Teams.
|
||||
2. Select a team.
|
||||
3. Select the External group sync tab and click on the `Add group` button.
|
||||
4. Insert LDAP distinguished name (DN) of LDAP group you want to synchronize with the team.
|
||||
5. Click on `Add group` button to save.
|
||||
|
||||
<div class="clearfix"></div>
|
67
docs/sources/enterprise/index.md
Normal file
67
docs/sources/enterprise/index.md
Normal file
@ -0,0 +1,67 @@
|
||||
+++
|
||||
title = "Grafana Enterprise"
|
||||
description = "Grafana Enterprise overview"
|
||||
keywords = ["grafana", "documentation", "datasource", "permissions", "ldap", "licensing", "enterprise"]
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "Grafana Enterprise"
|
||||
identifier = "enterprise"
|
||||
weight = 30
|
||||
+++
|
||||
|
||||
# Grafana Enterprise
|
||||
|
||||
Grafana Enterprise is a commercial edition of Grafana that includes additional features not found in the open source
|
||||
version.
|
||||
|
||||
Building on everything you already know and love about Grafana, Grafana Enterprise adds premium data sources,
|
||||
advanced authentication options, more permission controls, 24x7x365 support, and training from the core Grafana team.
|
||||
|
||||
Grafana Enterprise includes all of the features found in the open source edition and more.
|
||||
|
||||
___
|
||||
|
||||
### Enhanced LDAP Integration
|
||||
|
||||
With Grafana Enterprise you can set up synchronization between LDAP Groups and Teams. [Learn More]({{< relref "auth/enhanced_ldap.md" >}}).
|
||||
|
||||
### Datasource Permissions
|
||||
|
||||
Datasource permissions allow you to restrict query access to only specific Teams and Users. [Learn More]({{< relref "permissions/datasource_permissions.md" >}}).
|
||||
|
||||
### Premium Plugins
|
||||
|
||||
With a Grafana Enterprise licence you will get access to premium plugins, including:
|
||||
|
||||
* [Splunk](https://grafana.com/plugins/grafana-splunk-datasource)
|
||||
* [AppDynamics](https://grafana.com/plugins/dlopes7-appdynamics-datasource)
|
||||
* [DataDog](https://grafana.com/plugins/grafana-datadog-datasource)
|
||||
* [Dynatrace](https://grafana.com/plugins/grafana-dynatrace-datasource)
|
||||
* [New Relic](https://grafana.com/plugins/grafana-newrelic-datasource)
|
||||
|
||||
## Try Grafana Enterprise
|
||||
|
||||
You can learn more about Grafana Enterprise [here](https://grafana.com/enterprise). To purchase or obtain a trial license contact
|
||||
the Grafana Labs [Sales Team](https://grafana.com/contact?about=support&topic=Grafana%20Enterprise).
|
||||
|
||||
## License file management
|
||||
|
||||
To download your Grafana Enterprise license log in to your [Grafana.com](https://grafana.com) account and go to your **Org
|
||||
Profile**. In the side menu there is a section for Grafana Enterprise licenses. At the bottom of the license
|
||||
details page there is **Download Token** link that will download the *license.jwt* file containing your license.
|
||||
|
||||
Place the *license.jwt* file in Grafana's data folder. This is usually located at `/var/lib/grafana/data` on linux systems.
|
||||
|
||||
You can also configure a custom location for the license file via the ini setting:
|
||||
|
||||
```bash
|
||||
[enterprise]
|
||||
license_path = /company/secrets/license.jwt
|
||||
```
|
||||
|
||||
This setting can also be set via ENV variable which is useful if you're running Grafana via docker and have a custom
|
||||
volume where you have placed the license file. In this case set the ENV variable `GF_ENTERPRISE_LICENSE_PATH` to point
|
||||
to the location of your license file.
|
||||
|
||||
|
||||
|
@ -59,7 +59,7 @@ a time pattern for the index name or a wildcard.
|
||||
### Elasticsearch version
|
||||
|
||||
Be sure to specify your Elasticsearch version in the version selection dropdown. This is very important as there are differences how queries are composed.
|
||||
Currently the versions available is 2.x, 5.x and 5.6+ where 5.6+ means a version of 5.6 or higher, 6.3.2 for example.
|
||||
Currently the versions available is 2.x, 5.x, 5.6+ or 6.0+. 5.6+ means a version of 5.6 or less than 6.0. 6.0+ means a version of 6.0 or higher, 6.3.2 for example.
|
||||
|
||||
### Min time interval
|
||||
A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example `1m` if your data is written every minute.
|
||||
|
249
docs/sources/http_api/datasource_permissions.md
Normal file
249
docs/sources/http_api/datasource_permissions.md
Normal file
@ -0,0 +1,249 @@
|
||||
+++
|
||||
title = "Datasource Permissions HTTP API "
|
||||
description = "Grafana Datasource Permissions HTTP API"
|
||||
keywords = ["grafana", "http", "documentation", "api", "datasource", "permission", "permissions", "acl", "enterprise"]
|
||||
aliases = ["/http_api/datasourcepermissions/"]
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "Datasource Permissions"
|
||||
parent = "http_api"
|
||||
+++
|
||||
|
||||
# Datasource Permissions API
|
||||
|
||||
> Datasource Permissions is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "enterprise/index.md" >}}).
|
||||
|
||||
This API can be used to enable, disable, list, add and remove permissions for a datasource.
|
||||
|
||||
Permissions can be set for a user or a team. Permissions cannot be set for Admins - they always have access to everything.
|
||||
|
||||
The permission levels for the permission field:
|
||||
|
||||
- 1 = Query
|
||||
|
||||
## Enable permissions for a datasource
|
||||
|
||||
`POST /api/datasources/:id/enable-permissions`
|
||||
|
||||
Enables permissions for the datasource with the given `id`. No one except Org Admins will be able to query the datasource until permissions have been added which permit certain users or teams to query the datasource.
|
||||
|
||||
**Example request**:
|
||||
|
||||
```http
|
||||
POST /api/datasources/1/enable-permissions
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
{}
|
||||
```
|
||||
|
||||
**Example response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json; charset=UTF-8
|
||||
Content-Length: 35
|
||||
|
||||
{"message":"Datasource permissions enabled"}
|
||||
```
|
||||
|
||||
Status Codes:
|
||||
|
||||
- **200** - Ok
|
||||
- **400** - Permissions cannot be enabled, see response body for details
|
||||
- **401** - Unauthorized
|
||||
- **403** - Access denied
|
||||
- **404** - Datasource not found
|
||||
|
||||
## Disable permissions for a datasource
|
||||
|
||||
`POST /api/datasources/:id/disable-permissions`
|
||||
|
||||
Disables permissions for the datasource with the given `id`. All existing permissions will be removed and anyone will be able to query the datasource.
|
||||
|
||||
**Example request**:
|
||||
|
||||
```http
|
||||
POST /api/datasources/1/disable-permissions
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
{}
|
||||
```
|
||||
|
||||
**Example response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json; charset=UTF-8
|
||||
Content-Length: 35
|
||||
|
||||
{"message":"Datasource permissions disabled"}
|
||||
```
|
||||
|
||||
Status Codes:
|
||||
|
||||
- **200** - Ok
|
||||
- **400** - Permissions cannot be disabled, see response body for details
|
||||
- **401** - Unauthorized
|
||||
- **403** - Access denied
|
||||
- **404** - Datasource not found
|
||||
|
||||
## Get permissions for a datasource
|
||||
|
||||
`GET /api/datasources/:id/permissions`
|
||||
|
||||
Gets all existing permissions for the datasource with the given `id`.
|
||||
|
||||
**Example request**:
|
||||
|
||||
```http
|
||||
GET /api/datasources/1/permissions HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example Response**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json; charset=UTF-8
|
||||
Content-Length: 551
|
||||
|
||||
{
|
||||
"datasourceId": 1,
|
||||
"enabled": true,
|
||||
"permissions":
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"datasourceId": 1,
|
||||
"userId": 1,
|
||||
"userLogin": "user",
|
||||
"userEmail": "user@test.com",
|
||||
"userAvatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56",
|
||||
"permission": 1,
|
||||
"permissionName": "Query",
|
||||
"created": "2017-06-20T02:00:00+02:00",
|
||||
"updated": "2017-06-20T02:00:00+02:00",
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"datasourceId": 1,
|
||||
"teamId": 1,
|
||||
"team": "A Team",
|
||||
"teamAvatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56",
|
||||
"permission": 1,
|
||||
"permissionName": "Query",
|
||||
"created": "2017-06-20T02:00:00+02:00",
|
||||
"updated": "2017-06-20T02:00:00+02:00",
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Status Codes:
|
||||
|
||||
- **200** - Ok
|
||||
- **401** - Unauthorized
|
||||
- **403** - Access denied
|
||||
- **404** - Datasource not found
|
||||
|
||||
## Add permission for a datasource
|
||||
|
||||
`POST /api/datasources/:id/permissions`
|
||||
|
||||
Adds a user permission for the datasource with the given `id`.
|
||||
|
||||
**Example request**:
|
||||
|
||||
```http
|
||||
POST /api/datasources/1/permissions
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
{
|
||||
"userId": 1,
|
||||
"permission": 1
|
||||
}
|
||||
```
|
||||
|
||||
**Example response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json; charset=UTF-8
|
||||
Content-Length: 35
|
||||
|
||||
{"message":"Datasource permission added"}
|
||||
```
|
||||
|
||||
Adds a team permission for the datasource with the given `id`.
|
||||
|
||||
**Example request**:
|
||||
|
||||
```http
|
||||
POST /api/datasources/1/permissions
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
{
|
||||
"teamId": 1,
|
||||
"permission": 1
|
||||
}
|
||||
```
|
||||
|
||||
**Example response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json; charset=UTF-8
|
||||
Content-Length: 35
|
||||
|
||||
{"message":"Datasource permission added"}
|
||||
```
|
||||
|
||||
Status Codes:
|
||||
|
||||
- **200** - Ok
|
||||
- **400** - Permission cannot be added, see response body for details
|
||||
- **401** - Unauthorized
|
||||
- **403** - Access denied
|
||||
- **404** - Datasource not found
|
||||
|
||||
## Remove permission for a datasource
|
||||
|
||||
`DELETE /api/datasources/:id/permissions/:permissionId`
|
||||
|
||||
Removes the permission with the given `permissionId` for the datasource with the given `id`.
|
||||
|
||||
**Example request**:
|
||||
|
||||
```http
|
||||
DELETE /api/datasources/1/permissions/2
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json; charset=UTF-8
|
||||
Content-Length: 35
|
||||
|
||||
{"message":"Datasource permission removed"}
|
||||
```
|
||||
|
||||
Status Codes:
|
||||
|
||||
- **200** - Ok
|
||||
- **401** - Unauthorized
|
||||
- **403** - Access denied
|
||||
- **404** - Datasource not found or permission not found
|
111
docs/sources/http_api/external_group_sync.md
Normal file
111
docs/sources/http_api/external_group_sync.md
Normal file
@ -0,0 +1,111 @@
|
||||
+++
|
||||
title = "External Group Sync HTTP API "
|
||||
description = "Grafana External Group Sync HTTP API"
|
||||
keywords = ["grafana", "http", "documentation", "api", "team", "teams", "group", "member", "enterprise"]
|
||||
aliases = ["/http_api/external_group_sync/"]
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "External Group Sync"
|
||||
parent = "http_api"
|
||||
+++
|
||||
|
||||
# External Group Synchronization API
|
||||
|
||||
> External Group Synchronization is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "enterprise/index.md" >}}).
|
||||
|
||||
## Get External Groups
|
||||
|
||||
`GET /api/teams/:teamId/groups`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
GET /api/teams/1/groups HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic YWRtaW46YWRtaW4=
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
[
|
||||
{
|
||||
"orgId": 1,
|
||||
"teamId": 1,
|
||||
"groupId": "cn=editors,ou=groups,dc=grafana,dc=org"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Status Codes:
|
||||
|
||||
- **200** - Ok
|
||||
- **401** - Unauthorized
|
||||
- **403** - Permission denied
|
||||
|
||||
## Add External Group
|
||||
|
||||
`POST /api/teams/:teamId/groups`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
POST /api/teams/1/members HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic YWRtaW46YWRtaW4=
|
||||
|
||||
{
|
||||
"groupId": "cn=editors,ou=groups,dc=grafana,dc=org"
|
||||
}
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{"message":"Group added to Team"}
|
||||
```
|
||||
|
||||
Status Codes:
|
||||
|
||||
- **200** - Ok
|
||||
- **400** - Group is already added to this team
|
||||
- **401** - Unauthorized
|
||||
- **403** - Permission denied
|
||||
- **404** - Team not found
|
||||
|
||||
## Remove External Group
|
||||
|
||||
`DELETE /api/teams/:teamId/groups/:groupId`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
DELETE /api/teams/1/groups/cn=editors,ou=groups,dc=grafana,dc=org HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic YWRtaW46YWRtaW4=
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{"message":"Team Group removed"}
|
||||
```
|
||||
|
||||
Status Codes:
|
||||
|
||||
- **200** - Ok
|
||||
- **401** - Unauthorized
|
||||
- **403** - Permission denied
|
||||
- **404** - Team not found/Group not found
|
73
docs/sources/permissions/dashboard_folder_permissions.md
Normal file
73
docs/sources/permissions/dashboard_folder_permissions.md
Normal file
@ -0,0 +1,73 @@
|
||||
+++
|
||||
title = "Dashboard & Folder Permissions"
|
||||
description = "Grafana Dashboard & Folder Permissions Guide "
|
||||
keywords = ["grafana", "configuration", "documentation", "dashboard", "folder", "permissions", "teams"]
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "Dashboard & Folder"
|
||||
identifier = "dashboard-folder-permissions"
|
||||
parent = "permissions"
|
||||
weight = 3
|
||||
+++
|
||||
|
||||
# Dashboard & Folder Permissions
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v50/folder_permissions.png" max-width="500px" class="docs-image--right" >}}
|
||||
|
||||
For dashboards and dashboard folders there is a **Permissions** page that make it possible to
|
||||
remove the default role based permissions for Editors and Viewers. On this page you can add and assign permissions to specific **Users** and **Teams**.
|
||||
|
||||
You can assign & remove permissions for **Organization Roles**, **Users** and **Teams**.
|
||||
|
||||
Permission levels:
|
||||
|
||||
- **Admin**: Can edit & create dashboards and edit permissions.
|
||||
- **Edit**: Can edit & create dashboards. **Cannot** edit folder/dashboard permissions.
|
||||
- **View**: Can only view existing dashboards/folders.
|
||||
|
||||
## Restricting 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 **Org Admin Role**. Admins always have access to everything.
|
||||
- A more specific permission with a lower permission level will not have any effect if a more general rule exists with higher permission level. You need to remove or lower the permission level of the more general rule.
|
||||
|
||||
### How Grafana Resolves Multiple Permissions - Examples
|
||||
|
||||
#### Example 1 (`user1` has the Editor Role)
|
||||
|
||||
Permissions for a dashboard:
|
||||
|
||||
- `Everyone with Editor Role Can Edit`
|
||||
- `user1 Can View`
|
||||
|
||||
Result: `user1` has Edit permission as the highest permission always wins.
|
||||
|
||||
#### Example 2 (`user1` has the Viewer Role and is a member of `team1`)
|
||||
|
||||
Permissions for a dashboard:
|
||||
|
||||
- `Everyone with Viewer Role Can View`
|
||||
- `user1 Can Edit`
|
||||
- `team1 Can Admin`
|
||||
|
||||
Result: `user1` has Admin permission as the highest permission always wins.
|
||||
|
||||
#### Example 3
|
||||
|
||||
Permissions for a dashboard:
|
||||
|
||||
- `user1 Can Admin (inherited from parent folder)`
|
||||
- `user1 Can Edit`
|
||||
|
||||
Result: You cannot override to a lower permission. `user1` has Admin permission as the highest permission always wins.
|
||||
|
||||
## Summary
|
||||
|
||||
- **View**: Can only view existing dashboards/folders.
|
||||
- You cannot override permissions for users with **Org Admin Role**
|
||||
- A more specific permission with lower permission level will not have any effect if a more general rule exists with higher permission level.
|
||||
|
||||
For example if "Everyone with Editor Role Can Edit" exists in the ACL list then **John Doe** will still have Edit permission even after you have specifically added a permission for this user with the permission set to **View**. You need to remove or lower the permission level of the more general rule.
|
||||
- You cannot override permissions for users with **Org Admin Role**
|
||||
- A more specific permission with lower permission level will not have any effect if a more general rule exists with higher permission level. For example if "Everyone with Editor Role Can Edit" exists in the ACL list then **John Doe** will still have Edit permission even after you have specifically added a permission for this user with the permission set to **View**. You need to remove or lower the permission level of the more general rule.
|
71
docs/sources/permissions/datasource_permissions.md
Normal file
71
docs/sources/permissions/datasource_permissions.md
Normal file
@ -0,0 +1,71 @@
|
||||
+++
|
||||
title = "Datasource Permissions"
|
||||
description = "Grafana Datasource Permissions Guide "
|
||||
keywords = ["grafana", "configuration", "documentation", "datasource", "permissions", "users", "teams", "enterprise"]
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "Datasource"
|
||||
identifier = "datasource-permissions"
|
||||
parent = "permissions"
|
||||
weight = 4
|
||||
+++
|
||||
|
||||
# Datasource Permissions
|
||||
|
||||
> Datasource Permissions is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "enterprise/index.md" >}}).
|
||||
|
||||
Datasource permissions allows you to restrict access for users to query a datasource. For each datasource there is
|
||||
a permission page that makes it possible to enable permissions and restrict query permissions to specific
|
||||
**Users** and **Teams**.
|
||||
|
||||
## Restricting Access - Enable Permissions
|
||||
|
||||
{{< docs-imagebox img="/img/docs/enterprise/datasource_permissions_enable_still.png" class="docs-image--no-shadow docs-image--right" max-width= "600px" animated-gif="/img/docs/enterprise/datasource_permissions_enable.gif" >}}
|
||||
|
||||
By default, permissions are disabled for datasources and a datasource in an organization can be queried by any user in
|
||||
that organization. For example a user with `Viewer` role can still issue any possible query to a datasource, not just
|
||||
those queries that exist on dashboards he/she has access to.
|
||||
|
||||
When permissions are enabled for a datasource in an organization you will restrict admin and query access for that
|
||||
datasource to [admin users](/permissions/organization_roles/#admin-role) in that organization.
|
||||
|
||||
**To enable permissions for a datasource:**
|
||||
|
||||
1. Navigate to Configuration / Data Sources.
|
||||
2. Select the datasource you want to enable permissions for.
|
||||
3. Select the Permissions tab and click on the `Enable` button.
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
## Allow users and teams to query a datasource
|
||||
|
||||
{{< docs-imagebox img="/img/docs/enterprise/datasource_permissions_add_still.png" class="docs-image--no-shadow docs-image--right" max-width= "600px" animated-gif="/img/docs/enterprise/datasource_permissions_add.gif" >}}
|
||||
|
||||
After you have [enabled permissions](#restricting-access-enable-permissions) for a datasource you can assign query
|
||||
permissions to users and teams which will allow access to query the datasource.
|
||||
|
||||
**Assign query permission to users and teams:**
|
||||
|
||||
1. Navigate to Configuration / Data Sources.
|
||||
2. Select the datasource you want to assign query permissions for.
|
||||
3. Select the Permissions tab.
|
||||
4. click on the `Add Permission` button.
|
||||
5. Select Team/User and find the team/user you want to allow query access and click on the `Save` button.
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
## Restore Default Access - Disable Permissions
|
||||
|
||||
{{< docs-imagebox img="/img/docs/enterprise/datasource_permissions_disable_still.png" class="docs-image--no-shadow docs-image--right" max-width= "600px" animated-gif="/img/docs/enterprise/datasource_permissions_disable.gif" >}}
|
||||
|
||||
If you have enabled permissions for a datasource and want to return datasource permissions to the default, i.e.
|
||||
datasource can be queried by any user in that organization, you can disable permissions with a click of a button.
|
||||
Note that all existing permissions created for datasource will be deleted.
|
||||
|
||||
**To disable permissions for a datasource:**
|
||||
|
||||
1. Navigate to Configuration / Data Sources.
|
||||
2. Select the datasource you want to disable permissions for.
|
||||
3. Select the Permissions tab and click on the `Disable Permissions` button.
|
||||
|
||||
<div class="clearfix"></div>
|
12
docs/sources/permissions/index.md
Normal file
12
docs/sources/permissions/index.md
Normal file
@ -0,0 +1,12 @@
|
||||
+++
|
||||
title = "Permissions"
|
||||
description = "Permissions"
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "Permissions"
|
||||
identifier = "permissions"
|
||||
parent = "admin"
|
||||
weight = 3
|
||||
+++
|
||||
|
||||
|
38
docs/sources/permissions/organization_roles.md
Normal file
38
docs/sources/permissions/organization_roles.md
Normal file
@ -0,0 +1,38 @@
|
||||
+++
|
||||
title = "Organization Roles"
|
||||
description = "Grafana Organization Roles Guide "
|
||||
keywords = ["grafana", "configuration", "documentation", "organization", "roles", "permissions"]
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "Organization Roles"
|
||||
identifier = "organization-roles"
|
||||
parent = "permissions"
|
||||
weight = 2
|
||||
+++
|
||||
|
||||
# Organization Roles
|
||||
|
||||
Users can be 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.
|
||||
|
||||
## Admin Role
|
||||
|
||||
Can do everything scoped to the organization. For example:
|
||||
|
||||
- Add & Edit data sources.
|
||||
- Add & Edit organization users & teams.
|
||||
- Configure App plugins & set org settings.
|
||||
|
||||
## Editor Role
|
||||
|
||||
- Can create and modify dashboards & alert rules. This can be disabled on specific folders and dashboards.
|
||||
- **Cannot** create or edit data sources nor invite new users.
|
||||
|
||||
## Viewer Role
|
||||
|
||||
- View any dashboard. This can be disabled on specific folders and dashboards.
|
||||
- **Cannot** create or edit dashboards nor data sources.
|
||||
|
||||
This role can be tweaked via Grafana server setting [viewers_can_edit]({{< relref "installation/configuration.md#viewers-can-edit" >}}). If you set this to true users
|
||||
with **Viewer** can also make transient dashboard edits, meaning they can modify panels & queries but not save the changes (nor create new dashboards).
|
||||
Useful for public Grafana installations where you want anonymous users to be able to edit panels & queries but not save or create new dashboards.
|
42
docs/sources/permissions/overview.md
Normal file
42
docs/sources/permissions/overview.md
Normal file
@ -0,0 +1,42 @@
|
||||
+++
|
||||
title = "Overview"
|
||||
description = "Overview for permissions"
|
||||
keywords = ["grafana", "configuration", "documentation", "admin", "users", "datasources", "permissions"]
|
||||
type = "docs"
|
||||
aliases = ["/reference/admin", "/administration/permissions/"]
|
||||
[menu.docs]
|
||||
name = "Overview"
|
||||
identifier = "overview-permissions"
|
||||
parent = "permissions"
|
||||
weight = 1
|
||||
+++
|
||||
|
||||
# Permissions Overview
|
||||
|
||||
Grafana users have permissions that are determined by their:
|
||||
|
||||
- **Organization Role** (Admin, Editor, Viewer)
|
||||
- Via **Team** memberships where the **Team** has been assigned specific permissions.
|
||||
- Via permissions assigned directly to user (on folders, dashboards, datasources)
|
||||
- The Grafana Admin (i.e. Super Admin) user flag.
|
||||
|
||||
## Grafana Admin
|
||||
|
||||
This admin flag makes a user a `Super Admin`. This means they can access the `Server Admin` views where all users and organizations can be administrated.
|
||||
|
||||
## Organization Roles
|
||||
|
||||
Users can be 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. Learn more about [Organization Roles]({{< relref "permissions/organization_roles.md" >}}).
|
||||
|
||||
|
||||
## Dashboard & Folder Permissions
|
||||
|
||||
Dashboard and folder permissions allows you to remove the default role based permissions for Editors and Viewers and assign permissions to specific **Users** and **Teams**. Learn more about [Dashboard & Folder Permissions]({{< relref "permissions/dashboard_folder_permissions.md" >}}).
|
||||
|
||||
## Datasource Permissions
|
||||
|
||||
Per default, a datasource in an organization can be queried by any user in that organization. For example a user with `Viewer` role can still
|
||||
issue any possible query to a data source, not just those queries that exist on dashboards he/she has access to.
|
||||
|
||||
Datasource permissions allows you to change the default permissions for datasources and restrict query permissions to specific **Users** and **Teams**. Read more about [Datasource Permissions]({{< relref "permissions/datasource_permissions.md" >}}).
|
@ -12,7 +12,7 @@ weight = 9
|
||||
|
||||
If you have lots of metric names that change (new servers etc) in a defined pattern it is irritating to constantly have to create new dashboards.
|
||||
|
||||
With scripted dashboards you can dynamically create your dashboards using javascript. In the folder grafana install folder
|
||||
With scripted dashboards you can dynamically create your dashboards using javascript. In the grafana install folder
|
||||
under `public/dashboards/` there is a file named `scripted.js`. This file contains an example of a scripted dashboard. You can access it by using the url:
|
||||
`http://grafana_url/dashboard/script/scripted.js?rows=3&name=myName`
|
||||
|
||||
|
@ -3,7 +3,7 @@ title = "What's New in Grafana"
|
||||
[menu.docs]
|
||||
name = "What's New In Grafana"
|
||||
identifier = "whatsnew"
|
||||
weight = 3
|
||||
weight = 5
|
||||
+++
|
||||
|
||||
|
||||
|
@ -112,7 +112,7 @@ func (b *SearchRequestBuilder) Query() *QueryBuilder {
|
||||
|
||||
// Agg initiate and returns a new aggregation builder
|
||||
func (b *SearchRequestBuilder) Agg() AggBuilder {
|
||||
aggBuilder := newAggBuilder()
|
||||
aggBuilder := newAggBuilder(b.version)
|
||||
b.aggBuilders = append(b.aggBuilders, aggBuilder)
|
||||
return aggBuilder
|
||||
}
|
||||
@ -275,11 +275,13 @@ type AggBuilder interface {
|
||||
type aggBuilderImpl struct {
|
||||
AggBuilder
|
||||
aggDefs []*aggDef
|
||||
version int
|
||||
}
|
||||
|
||||
func newAggBuilder() *aggBuilderImpl {
|
||||
func newAggBuilder(version int) *aggBuilderImpl {
|
||||
return &aggBuilderImpl{
|
||||
aggDefs: make([]*aggDef, 0),
|
||||
version: version,
|
||||
}
|
||||
}
|
||||
|
||||
@ -317,7 +319,7 @@ func (b *aggBuilderImpl) Histogram(key, field string, fn func(a *HistogramAgg, b
|
||||
})
|
||||
|
||||
if fn != nil {
|
||||
builder := newAggBuilder()
|
||||
builder := newAggBuilder(b.version)
|
||||
aggDef.builders = append(aggDef.builders, builder)
|
||||
fn(innerAgg, builder)
|
||||
}
|
||||
@ -337,7 +339,7 @@ func (b *aggBuilderImpl) DateHistogram(key, field string, fn func(a *DateHistogr
|
||||
})
|
||||
|
||||
if fn != nil {
|
||||
builder := newAggBuilder()
|
||||
builder := newAggBuilder(b.version)
|
||||
aggDef.builders = append(aggDef.builders, builder)
|
||||
fn(innerAgg, builder)
|
||||
}
|
||||
@ -347,6 +349,8 @@ func (b *aggBuilderImpl) DateHistogram(key, field string, fn func(a *DateHistogr
|
||||
return b
|
||||
}
|
||||
|
||||
const termsOrderTerm = "_term"
|
||||
|
||||
func (b *aggBuilderImpl) Terms(key, field string, fn func(a *TermsAggregation, b AggBuilder)) AggBuilder {
|
||||
innerAgg := &TermsAggregation{
|
||||
Field: field,
|
||||
@ -358,11 +362,18 @@ func (b *aggBuilderImpl) Terms(key, field string, fn func(a *TermsAggregation, b
|
||||
})
|
||||
|
||||
if fn != nil {
|
||||
builder := newAggBuilder()
|
||||
builder := newAggBuilder(b.version)
|
||||
aggDef.builders = append(aggDef.builders, builder)
|
||||
fn(innerAgg, builder)
|
||||
}
|
||||
|
||||
if b.version >= 60 && len(innerAgg.Order) > 0 {
|
||||
if orderBy, exists := innerAgg.Order[termsOrderTerm]; exists {
|
||||
innerAgg.Order["_key"] = orderBy
|
||||
delete(innerAgg.Order, termsOrderTerm)
|
||||
}
|
||||
}
|
||||
|
||||
b.aggDefs = append(b.aggDefs, aggDef)
|
||||
|
||||
return b
|
||||
@ -377,7 +388,7 @@ func (b *aggBuilderImpl) Filters(key string, fn func(a *FiltersAggregation, b Ag
|
||||
Aggregation: innerAgg,
|
||||
})
|
||||
if fn != nil {
|
||||
builder := newAggBuilder()
|
||||
builder := newAggBuilder(b.version)
|
||||
aggDef.builders = append(aggDef.builders, builder)
|
||||
fn(innerAgg, builder)
|
||||
}
|
||||
@ -398,7 +409,7 @@ func (b *aggBuilderImpl) GeoHashGrid(key, field string, fn func(a *GeoHashGridAg
|
||||
})
|
||||
|
||||
if fn != nil {
|
||||
builder := newAggBuilder()
|
||||
builder := newAggBuilder(b.version)
|
||||
aggDef.builders = append(aggDef.builders, builder)
|
||||
fn(innerAgg, builder)
|
||||
}
|
||||
|
@ -127,6 +127,60 @@ func TestExecuteTimeSeriesQuery(t *testing.T) {
|
||||
So(avgAgg.Aggregation.Type, ShouldEqual, "avg")
|
||||
})
|
||||
|
||||
Convey("With term agg and order by term", func() {
|
||||
c := newFakeClient(5)
|
||||
_, err := executeTsdbQuery(c, `{
|
||||
"timeField": "@timestamp",
|
||||
"bucketAggs": [
|
||||
{
|
||||
"type": "terms",
|
||||
"field": "@host",
|
||||
"id": "2",
|
||||
"settings": { "size": "5", "order": "asc", "orderBy": "_term" }
|
||||
},
|
||||
{ "type": "date_histogram", "field": "@timestamp", "id": "3" }
|
||||
],
|
||||
"metrics": [
|
||||
{"type": "count", "id": "1" },
|
||||
{"type": "avg", "field": "@value", "id": "5" }
|
||||
]
|
||||
}`, from, to, 15*time.Second)
|
||||
So(err, ShouldBeNil)
|
||||
sr := c.multisearchRequests[0].Requests[0]
|
||||
|
||||
firstLevel := sr.Aggs[0]
|
||||
So(firstLevel.Key, ShouldEqual, "2")
|
||||
termsAgg := firstLevel.Aggregation.Aggregation.(*es.TermsAggregation)
|
||||
So(termsAgg.Order["_term"], ShouldEqual, "asc")
|
||||
})
|
||||
|
||||
Convey("With term agg and order by term with es6.x", func() {
|
||||
c := newFakeClient(60)
|
||||
_, err := executeTsdbQuery(c, `{
|
||||
"timeField": "@timestamp",
|
||||
"bucketAggs": [
|
||||
{
|
||||
"type": "terms",
|
||||
"field": "@host",
|
||||
"id": "2",
|
||||
"settings": { "size": "5", "order": "asc", "orderBy": "_term" }
|
||||
},
|
||||
{ "type": "date_histogram", "field": "@timestamp", "id": "3" }
|
||||
],
|
||||
"metrics": [
|
||||
{"type": "count", "id": "1" },
|
||||
{"type": "avg", "field": "@value", "id": "5" }
|
||||
]
|
||||
}`, from, to, 15*time.Second)
|
||||
So(err, ShouldBeNil)
|
||||
sr := c.multisearchRequests[0].Requests[0]
|
||||
|
||||
firstLevel := sr.Aggs[0]
|
||||
So(firstLevel.Key, ShouldEqual, "2")
|
||||
termsAgg := firstLevel.Aggregation.Aggregation.(*es.TermsAggregation)
|
||||
So(termsAgg.Order["_key"], ShouldEqual, "asc")
|
||||
})
|
||||
|
||||
Convey("With metric percentiles", func() {
|
||||
c := newFakeClient(5)
|
||||
_, err := executeTsdbQuery(c, `{
|
||||
|
@ -4,13 +4,13 @@ import coreModule from '../core_module';
|
||||
export class JsonEditorCtrl {
|
||||
/** @ngInject */
|
||||
constructor($scope) {
|
||||
$scope.json = angular.toJson($scope.object, true);
|
||||
$scope.canUpdate = $scope.updateHandler !== void 0 && $scope.contextSrv.isEditor;
|
||||
$scope.canCopy = $scope.enableCopy;
|
||||
$scope.json = angular.toJson($scope.model.object, true);
|
||||
$scope.canUpdate = $scope.model.updateHandler !== void 0 && $scope.contextSrv.isEditor;
|
||||
$scope.canCopy = $scope.model.enableCopy;
|
||||
|
||||
$scope.update = () => {
|
||||
const newObject = angular.fromJson($scope.json);
|
||||
$scope.updateHandler(newObject, $scope.object);
|
||||
$scope.model.updateHandler(newObject, $scope.model.object);
|
||||
};
|
||||
|
||||
$scope.getContentForClipboard = () => $scope.json;
|
||||
|
@ -24,7 +24,9 @@ export const locationReducer = (state = initialState, action: Action): LocationS
|
||||
return {
|
||||
url: renderUrl(path || state.path, query),
|
||||
path: path || state.path,
|
||||
query: query,
|
||||
query: {
|
||||
...query,
|
||||
},
|
||||
routeParams: routeParams || state.routeParams,
|
||||
};
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import { store } from 'app/store/configureStore';
|
||||
import locationUtil from 'app/core/utils/location_util';
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
|
||||
// Services that handles angular -> mobx store sync & other react <-> angular sync
|
||||
// Services that handles angular -> redux store sync & other react <-> angular sync
|
||||
export class BridgeSrv {
|
||||
private fullPageReloadRoutes;
|
||||
|
||||
|
@ -2,13 +2,13 @@
|
||||
import config from 'app/core/config';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { removePanel } from 'app/features/dashboard/utils/panel';
|
||||
|
||||
// Services
|
||||
import { AnnotationsSrv } from '../annotations/annotations_srv';
|
||||
|
||||
// Types
|
||||
import { DashboardModel } from './dashboard_model';
|
||||
import { PanelModel } from './panel_model';
|
||||
|
||||
export class DashboardCtrl {
|
||||
dashboard: DashboardModel;
|
||||
@ -19,7 +19,6 @@ export class DashboardCtrl {
|
||||
/** @ngInject */
|
||||
constructor(
|
||||
private $scope,
|
||||
private $rootScope,
|
||||
private keybindingSrv,
|
||||
private timeSrv,
|
||||
private variableSrv,
|
||||
@ -112,12 +111,14 @@ export class DashboardCtrl {
|
||||
}
|
||||
|
||||
showJsonEditor(evt, options) {
|
||||
const editScope = this.$rootScope.$new();
|
||||
editScope.object = options.object;
|
||||
editScope.updateHandler = options.updateHandler;
|
||||
const model = {
|
||||
object: options.object,
|
||||
updateHandler: options.updateHandler,
|
||||
};
|
||||
|
||||
this.$scope.appEvent('show-dash-editor', {
|
||||
src: 'public/app/partials/edit_json.html',
|
||||
scope: editScope,
|
||||
model: model,
|
||||
});
|
||||
}
|
||||
|
||||
@ -136,34 +137,7 @@ export class DashboardCtrl {
|
||||
}
|
||||
|
||||
const panelInfo = this.dashboard.getPanelInfoById(options.panelId);
|
||||
this.removePanel(panelInfo.panel, true);
|
||||
}
|
||||
|
||||
removePanel(panel: PanelModel, ask: boolean) {
|
||||
// confirm deletion
|
||||
if (ask !== false) {
|
||||
let text2, confirmText;
|
||||
|
||||
if (panel.alert) {
|
||||
text2 = 'Panel includes an alert rule, removing panel will also remove alert rule';
|
||||
confirmText = 'YES';
|
||||
}
|
||||
|
||||
this.$scope.appEvent('confirm-modal', {
|
||||
title: 'Remove Panel',
|
||||
text: 'Are you sure you want to remove this panel?',
|
||||
text2: text2,
|
||||
icon: 'fa-trash',
|
||||
confirmText: confirmText,
|
||||
yesText: 'Remove',
|
||||
onConfirm: () => {
|
||||
this.removePanel(panel, false);
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.dashboard.removePanel(panel);
|
||||
removePanel(this.dashboard, panelInfo.panel, true);
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
|
@ -232,11 +232,6 @@ export class DashboardModel {
|
||||
return this.meta.fullscreen && !panel.fullscreen;
|
||||
}
|
||||
|
||||
changePanelType(panel: PanelModel, pluginId: string) {
|
||||
panel.changeType(pluginId);
|
||||
this.events.emit('panel-type-changed', panel);
|
||||
}
|
||||
|
||||
private ensureListExist(data) {
|
||||
if (!data) {
|
||||
data = {};
|
||||
|
@ -83,7 +83,6 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
|
||||
dashboard.on('view-mode-changed', this.onViewModeChanged.bind(this));
|
||||
dashboard.on('row-collapsed', this.triggerForceUpdate.bind(this));
|
||||
dashboard.on('row-expanded', this.triggerForceUpdate.bind(this));
|
||||
dashboard.on('panel-type-changed', this.triggerForceUpdate.bind(this));
|
||||
}
|
||||
|
||||
buildLayout() {
|
||||
@ -176,7 +175,12 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
|
||||
const panelClasses = classNames({ panel: true, 'panel--fullscreen': panel.fullscreen });
|
||||
panelElements.push(
|
||||
<div key={panel.id.toString()} className={panelClasses} id={`panel-${panel.id}`}>
|
||||
<DashboardPanel panel={panel} dashboard={this.props.dashboard} panelType={panel.type} />
|
||||
<DashboardPanel
|
||||
panel={panel}
|
||||
dashboard={this.props.dashboard}
|
||||
isEditing={panel.isEditing}
|
||||
isFullscreen={panel.fullscreen}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { PureComponent } from 'react';
|
||||
import config from 'app/core/config';
|
||||
|
||||
import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
|
||||
@ -14,16 +14,17 @@ import { PanelModel } from '../panel_model';
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
|
||||
export interface Props {
|
||||
panelType: string;
|
||||
panel: PanelModel;
|
||||
dashboard: DashboardModel;
|
||||
isEditing: boolean;
|
||||
isFullscreen: boolean;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
plugin: PanelPlugin;
|
||||
}
|
||||
|
||||
export class DashboardPanel extends React.Component<Props, State> {
|
||||
export class DashboardPanel extends PureComponent<Props, State> {
|
||||
element: any;
|
||||
angularPanel: AngularComponent;
|
||||
specialPanels = {};
|
||||
@ -119,9 +120,8 @@ export class DashboardPanel extends React.Component<Props, State> {
|
||||
const { dashboard, panel } = this.props;
|
||||
const { plugin } = this.state;
|
||||
|
||||
const containerClass = panel.isEditing ? 'panel-editor-container' : 'panel-height-helper';
|
||||
const panelWrapperClass = panel.isEditing ? 'panel-editor-container__panel' : 'panel-height-helper';
|
||||
|
||||
const containerClass = this.props.isEditing ? 'panel-editor-container' : 'panel-height-helper';
|
||||
const panelWrapperClass = this.props.isEditing ? 'panel-editor-container__panel' : 'panel-height-helper';
|
||||
// this might look strange with these classes that change when edit, but
|
||||
// I want to try to keep markup (parents) for panel the same in edit mode to avoide unmount / new mount of panel
|
||||
return (
|
||||
|
@ -5,7 +5,7 @@ import React, { ComponentClass, PureComponent } from 'react';
|
||||
import { getTimeSrv } from '../time_srv';
|
||||
|
||||
// Components
|
||||
import { PanelHeader } from './PanelHeader';
|
||||
import { PanelHeader } from './PanelHeader/PanelHeader';
|
||||
import { DataPanel } from './DataPanel';
|
||||
|
||||
// Types
|
||||
@ -49,17 +49,19 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
const timeSrv = getTimeSrv();
|
||||
const timeRange = timeSrv.timeRange();
|
||||
|
||||
this.setState({
|
||||
this.setState(prevState => ({
|
||||
...prevState,
|
||||
refreshCounter: this.state.refreshCounter + 1,
|
||||
timeRange: timeRange,
|
||||
});
|
||||
}));
|
||||
};
|
||||
|
||||
onRender = () => {
|
||||
console.log('onRender');
|
||||
this.setState({
|
||||
this.setState(prevState => ({
|
||||
...prevState,
|
||||
renderCounter: this.state.renderCounter + 1,
|
||||
});
|
||||
}));
|
||||
};
|
||||
|
||||
get isVisible() {
|
||||
@ -68,12 +70,12 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
|
||||
render() {
|
||||
const { panel, dashboard } = this.props;
|
||||
const { refreshCounter, timeRange, renderCounter } = this.state;
|
||||
|
||||
const { datasource, targets } = panel;
|
||||
const { timeRange, renderCounter, refreshCounter } = this.state;
|
||||
const PanelComponent = this.props.component;
|
||||
|
||||
console.log('Panel chrome render');
|
||||
|
||||
console.log('panelChrome render');
|
||||
return (
|
||||
<div className="panel-container">
|
||||
<PanelHeader panel={panel} dashboard={dashboard} />
|
||||
|
@ -1,83 +0,0 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { PanelModel } from '../panel_model';
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
import { store } from 'app/store/configureStore';
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
|
||||
interface PanelHeaderProps {
|
||||
panel: PanelModel;
|
||||
dashboard: DashboardModel;
|
||||
}
|
||||
|
||||
export class PanelHeader extends React.Component<PanelHeaderProps, any> {
|
||||
onEditPanel = () => {
|
||||
store.dispatch(
|
||||
updateLocation({
|
||||
query: {
|
||||
panelId: this.props.panel.id,
|
||||
edit: true,
|
||||
fullscreen: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
onViewPanel = () => {
|
||||
store.dispatch(
|
||||
updateLocation({
|
||||
query: {
|
||||
panelId: this.props.panel.id,
|
||||
edit: false,
|
||||
fullscreen: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const isFullscreen = false;
|
||||
const isLoading = false;
|
||||
const panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen });
|
||||
|
||||
return (
|
||||
<div className={panelHeaderClass}>
|
||||
<span className="panel-info-corner">
|
||||
<i className="fa" />
|
||||
<span className="panel-info-corner-inner" />
|
||||
</span>
|
||||
|
||||
{isLoading && (
|
||||
<span className="panel-loading">
|
||||
<i className="fa fa-spinner fa-spin" />
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div className="panel-title-container">
|
||||
<span className="panel-title">
|
||||
<span className="icon-gf panel-alert-icon" />
|
||||
<span className="panel-title-text">{this.props.panel.title}</span>
|
||||
<span className="panel-menu-container dropdown">
|
||||
<span className="fa fa-caret-down panel-menu-toggle" data-toggle="dropdown" />
|
||||
<ul className="dropdown-menu dropdown-menu--menu panel-menu" role="menu">
|
||||
<li>
|
||||
<a onClick={this.onEditPanel}>
|
||||
<i className="fa fa-fw fa-edit" /> Edit
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a onClick={this.onViewPanel}>
|
||||
<i className="fa fa-fw fa-eye" /> View
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
<span className="panel-time-info">
|
||||
<i className="fa fa-clock-o" /> 4m
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { PanelHeaderMenu } from './PanelHeaderMenu';
|
||||
|
||||
import { DashboardModel } from 'app/features/dashboard/dashboard_model';
|
||||
import { PanelModel } from 'app/features/dashboard/panel_model';
|
||||
|
||||
export interface Props {
|
||||
panel: PanelModel;
|
||||
dashboard: DashboardModel;
|
||||
}
|
||||
|
||||
export class PanelHeader extends PureComponent<Props> {
|
||||
render() {
|
||||
const isFullscreen = false;
|
||||
const isLoading = false;
|
||||
const panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen });
|
||||
const { panel, dashboard } = this.props;
|
||||
|
||||
return (
|
||||
<div className={panelHeaderClass}>
|
||||
<span className="panel-info-corner">
|
||||
<i className="fa" />
|
||||
<span className="panel-info-corner-inner" />
|
||||
</span>
|
||||
|
||||
{isLoading && (
|
||||
<span className="panel-loading">
|
||||
<i className="fa fa-spinner fa-spin" />
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div className="panel-title-container">
|
||||
<div className="panel-title">
|
||||
<span className="icon-gf panel-alert-icon" />
|
||||
<span className="panel-title-text" data-toggle="dropdown">
|
||||
{panel.title} <span className="fa fa-caret-down panel-menu-toggle" />
|
||||
</span>
|
||||
|
||||
<PanelHeaderMenu panel={panel} dashboard={dashboard} />
|
||||
|
||||
<span className="panel-time-info">
|
||||
<i className="fa fa-clock-o" /> 4m
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { DashboardModel } from 'app/features/dashboard/dashboard_model';
|
||||
import { PanelModel } from 'app/features/dashboard/panel_model';
|
||||
import { PanelHeaderMenuItem } from './PanelHeaderMenuItem';
|
||||
import { getPanelMenu } from 'app/features/dashboard/utils/getPanelMenu';
|
||||
import { PanelMenuItem } from 'app/types/panel';
|
||||
|
||||
export interface Props {
|
||||
panel: PanelModel;
|
||||
dashboard: DashboardModel;
|
||||
}
|
||||
|
||||
export class PanelHeaderMenu extends PureComponent<Props> {
|
||||
renderItems = (menu: PanelMenuItem[], isSubMenu = false) => {
|
||||
return (
|
||||
<ul className="dropdown-menu dropdown-menu--menu panel-menu" role={isSubMenu ? '' : 'menu'}>
|
||||
{menu.map((menuItem, idx: number) => {
|
||||
return (
|
||||
<PanelHeaderMenuItem
|
||||
key={`${menuItem.text}${idx}`}
|
||||
type={menuItem.type}
|
||||
text={menuItem.text}
|
||||
iconClassName={menuItem.iconClassName}
|
||||
onClick={menuItem.onClick}
|
||||
shortcut={menuItem.shortcut}
|
||||
>
|
||||
{menuItem.subMenu && this.renderItems(menuItem.subMenu, true)}
|
||||
</PanelHeaderMenuItem>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { dashboard, panel } = this.props;
|
||||
const menu = getPanelMenu(dashboard, panel);
|
||||
return <div className="panel-menu-container dropdown">{this.renderItems(menu)}</div>;
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
import React, { SFC } from 'react';
|
||||
import { PanelMenuItem } from 'app/types/panel';
|
||||
|
||||
interface Props {
|
||||
children: any;
|
||||
}
|
||||
|
||||
export const PanelHeaderMenuItem: SFC<Props & PanelMenuItem> = props => {
|
||||
const isSubMenu = props.type === 'submenu';
|
||||
const isDivider = props.type === 'divider';
|
||||
return isDivider ? (
|
||||
<li className="divider" />
|
||||
) : (
|
||||
<li className={isSubMenu ? 'dropdown-submenu' : null}>
|
||||
<a onClick={props.onClick}>
|
||||
{props.iconClassName && <i className={props.iconClassName} />}
|
||||
<span className="dropdown-item-text">{props.text}</span>
|
||||
{props.shortcut && <span className="dropdown-menu-item-shortcut">{props.shortcut}</span>}
|
||||
</a>
|
||||
{props.children}
|
||||
</li>
|
||||
);
|
||||
};
|
@ -1,25 +1,21 @@
|
||||
|
||||
<!-- <p> -->
|
||||
<!-- Exporting will export a cleaned sharable dashboard that can be imported -->
|
||||
<!-- into another Grafana instance. -->
|
||||
<!-- </p> -->
|
||||
|
||||
<div class="share-modal-header">
|
||||
<div class="share-modal-big-icon">
|
||||
<i class="fa fa-cloud-upload"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="share-modal-info-text">
|
||||
Export the dashboard to a JSON file. The exporter will templatize the
|
||||
dashboard's data sources to make it easy for others to import and reuse.
|
||||
You can share dashboards on <a class="external-link" href="https://grafana.com">Grafana.com</a>
|
||||
</p>
|
||||
<gf-form-switch
|
||||
class="gf-form"
|
||||
label="Export for sharing externally"
|
||||
label-class="width-16"
|
||||
checked="ctrl.shareExternally"
|
||||
tooltip="Useful for sharing dashboard publicly on grafana.com. Will templatize data source names. Can then only be used with the specific dashboard import API.">
|
||||
</gf-form-switch>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button type="button" class="btn gf-form-btn width-10 btn-success" ng-click="ctrl.save()">
|
||||
<button type="button" class="btn gf-form-btn width-10 btn-success" ng-click="ctrl.saveDashboardAsFile()">
|
||||
<i class="fa fa-save"></i> Save to file
|
||||
</button>
|
||||
<button type="button" class="btn gf-form-btn width-10 btn-secondary" ng-click="ctrl.saveJson()">
|
||||
<button type="button" class="btn gf-form-btn width-10 btn-secondary" ng-click="ctrl.viewJson()">
|
||||
<i class="fa fa-file-text-o"></i> View JSON
|
||||
</button>
|
||||
<a class="btn btn-link" ng-click="ctrl.dismiss()">Cancel</a>
|
||||
|
@ -8,34 +8,55 @@ export class DashExportCtrl {
|
||||
dash: any;
|
||||
exporter: DashboardExporter;
|
||||
dismiss: () => void;
|
||||
shareExternally: boolean;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private dashboardSrv, datasourceSrv, private $scope, private $rootScope) {
|
||||
this.exporter = new DashboardExporter(datasourceSrv);
|
||||
|
||||
this.exporter.makeExportable(this.dashboardSrv.getCurrent()).then(dash => {
|
||||
this.$scope.$apply(() => {
|
||||
this.dash = dash;
|
||||
});
|
||||
});
|
||||
this.dash = this.dashboardSrv.getCurrent();
|
||||
}
|
||||
|
||||
save() {
|
||||
const blob = new Blob([angular.toJson(this.dash, true)], {
|
||||
saveDashboardAsFile() {
|
||||
if (this.shareExternally) {
|
||||
this.exporter.makeExportable(this.dash).then((dashboardJson: any) => {
|
||||
this.$scope.$apply(() => {
|
||||
this.openSaveAsDialog(dashboardJson);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.openSaveAsDialog(this.dash.getSaveModelClone());
|
||||
}
|
||||
}
|
||||
|
||||
viewJson() {
|
||||
if (this.shareExternally) {
|
||||
this.exporter.makeExportable(this.dash).then((dashboardJson: any) => {
|
||||
this.$scope.$apply(() => {
|
||||
this.openJsonModal(dashboardJson);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.openJsonModal(this.dash.getSaveModelClone());
|
||||
}
|
||||
}
|
||||
|
||||
private openSaveAsDialog(dash: any) {
|
||||
const blob = new Blob([angular.toJson(dash, true)], {
|
||||
type: 'application/json;charset=utf-8',
|
||||
});
|
||||
saveAs(blob, this.dash.title + '-' + new Date().getTime() + '.json');
|
||||
saveAs(blob, dash.title + '-' + new Date().getTime() + '.json');
|
||||
}
|
||||
|
||||
saveJson() {
|
||||
const clone = this.dash;
|
||||
const editScope = this.$rootScope.$new();
|
||||
editScope.object = clone;
|
||||
editScope.enableCopy = true;
|
||||
private openJsonModal(clone: object) {
|
||||
const model = {
|
||||
object: clone,
|
||||
enableCopy: true,
|
||||
};
|
||||
|
||||
this.$rootScope.appEvent('show-modal', {
|
||||
src: 'public/app/partials/edit_json.html',
|
||||
scope: editScope,
|
||||
model: model,
|
||||
});
|
||||
|
||||
this.dismiss();
|
||||
|
@ -29,19 +29,36 @@ export class DashboardExporter {
|
||||
}
|
||||
|
||||
const templateizeDatasourceUsage = obj => {
|
||||
let datasource = obj.datasource;
|
||||
let datasourceVariable = null;
|
||||
|
||||
// ignore data source properties that contain a variable
|
||||
if (obj.datasource && obj.datasource.indexOf('$') === 0) {
|
||||
if (variableLookup[obj.datasource.substring(1)]) {
|
||||
return;
|
||||
if (datasource && datasource.indexOf('$') === 0) {
|
||||
datasourceVariable = variableLookup[datasource.substring(1)];
|
||||
if (datasourceVariable && datasourceVariable.current) {
|
||||
datasource = datasourceVariable.current.value;
|
||||
}
|
||||
}
|
||||
|
||||
promises.push(
|
||||
this.datasourceSrv.get(obj.datasource).then(ds => {
|
||||
this.datasourceSrv.get(datasource).then(ds => {
|
||||
if (ds.meta.builtIn) {
|
||||
return;
|
||||
}
|
||||
|
||||
// add data source type to require list
|
||||
requires['datasource' + ds.meta.id] = {
|
||||
type: 'datasource',
|
||||
id: ds.meta.id,
|
||||
name: ds.meta.name,
|
||||
version: ds.meta.info.version || '1.0.0',
|
||||
};
|
||||
|
||||
// if used via variable we can skip templatizing usage
|
||||
if (datasourceVariable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const refName = 'DS_' + ds.name.replace(' ', '_').toUpperCase();
|
||||
datasources[refName] = {
|
||||
name: refName,
|
||||
@ -51,14 +68,8 @@ export class DashboardExporter {
|
||||
pluginId: ds.meta.id,
|
||||
pluginName: ds.meta.name,
|
||||
};
|
||||
obj.datasource = '${' + refName + '}';
|
||||
|
||||
requires['datasource' + ds.meta.id] = {
|
||||
type: 'datasource',
|
||||
id: ds.meta.id,
|
||||
name: ds.meta.name,
|
||||
version: ds.meta.info.version || '1.0.0',
|
||||
};
|
||||
obj.datasource = '${' + refName + '}';
|
||||
})
|
||||
);
|
||||
};
|
||||
|
@ -12,6 +12,8 @@ export function ShareModalCtrl($scope, $rootScope, $location, $timeout, timeSrv,
|
||||
$scope.editor = { index: $scope.tabIndex || 0 };
|
||||
|
||||
$scope.init = () => {
|
||||
$scope.panel = $scope.model && $scope.model.panel ? $scope.model.panel : $scope.panel; // React pass panel and dashboard in the "model" property
|
||||
$scope.dashboard = $scope.model && $scope.model.dashboard ? $scope.model.dashboard : $scope.dashboard; // ^
|
||||
$scope.modeSharePanel = $scope.panel ? true : false;
|
||||
|
||||
$scope.tabs = [{ title: 'Link', src: 'shareLink.html' }];
|
||||
|
@ -32,8 +32,8 @@ describe('given dashboard with repeated panels', () => {
|
||||
{
|
||||
name: 'ds',
|
||||
type: 'datasource',
|
||||
query: 'testdb',
|
||||
current: { value: 'prod', text: 'prod' },
|
||||
query: 'other2',
|
||||
current: { value: 'other2', text: 'other2' },
|
||||
options: [],
|
||||
},
|
||||
],
|
||||
@ -205,6 +205,11 @@ describe('given dashboard with repeated panels', () => {
|
||||
expect(variable.options[0].text).toBe('${VAR_PREFIX}');
|
||||
expect(variable.options[0].value).toBe('${VAR_PREFIX}');
|
||||
});
|
||||
|
||||
it('should add datasources only use via datasource variable to requires', () => {
|
||||
const require = _.find(exported.__requires, { name: 'OtherDB_2' });
|
||||
expect(require.id).toBe('other2');
|
||||
});
|
||||
});
|
||||
|
||||
// Stub responses
|
||||
@ -219,6 +224,11 @@ stubs['other'] = {
|
||||
meta: { id: 'other', info: { version: '1.2.1' }, name: 'OtherDB' },
|
||||
};
|
||||
|
||||
stubs['other2'] = {
|
||||
name: 'other2',
|
||||
meta: { id: 'other2', info: { version: '1.2.1' }, name: 'OtherDB_2' },
|
||||
};
|
||||
|
||||
stubs['-- Mixed --'] = {
|
||||
name: 'mixed',
|
||||
meta: {
|
||||
|
120
public/app/features/dashboard/utils/getPanelMenu.ts
Normal file
120
public/app/features/dashboard/utils/getPanelMenu.ts
Normal file
@ -0,0 +1,120 @@
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
import { store } from 'app/store/configureStore';
|
||||
|
||||
import { removePanel, duplicatePanel, copyPanel, editPanelJson, sharePanel } from 'app/features/dashboard/utils/panel';
|
||||
import { PanelModel } from 'app/features/dashboard/panel_model';
|
||||
import { DashboardModel } from 'app/features/dashboard/dashboard_model';
|
||||
import { PanelMenuItem } from 'app/types/panel';
|
||||
|
||||
export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => {
|
||||
const onViewPanel = () => {
|
||||
store.dispatch(
|
||||
updateLocation({
|
||||
query: {
|
||||
panelId: panel.id,
|
||||
edit: false,
|
||||
fullscreen: true,
|
||||
},
|
||||
partial: true,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onEditPanel = () => {
|
||||
store.dispatch(
|
||||
updateLocation({
|
||||
query: {
|
||||
panelId: panel.id,
|
||||
edit: true,
|
||||
fullscreen: true,
|
||||
},
|
||||
partial: true,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onSharePanel = () => {
|
||||
sharePanel(dashboard, panel);
|
||||
};
|
||||
|
||||
const onDuplicatePanel = () => {
|
||||
duplicatePanel(dashboard, panel);
|
||||
};
|
||||
|
||||
const onCopyPanel = () => {
|
||||
copyPanel(panel);
|
||||
};
|
||||
|
||||
const onEditPanelJson = () => {
|
||||
editPanelJson(dashboard, panel);
|
||||
};
|
||||
|
||||
const onRemovePanel = () => {
|
||||
removePanel(dashboard, panel, true);
|
||||
};
|
||||
|
||||
const menu: PanelMenuItem[] = [];
|
||||
|
||||
menu.push({
|
||||
text: 'View',
|
||||
iconClassName: 'fa fa-fw fa-eye',
|
||||
onClick: onViewPanel,
|
||||
shortcut: 'v',
|
||||
});
|
||||
|
||||
if (dashboard.meta.canEdit) {
|
||||
menu.push({
|
||||
text: 'Edit',
|
||||
iconClassName: 'fa fa-fw fa-edit',
|
||||
onClick: onEditPanel,
|
||||
shortcut: 'e',
|
||||
});
|
||||
}
|
||||
|
||||
menu.push({
|
||||
text: 'Share',
|
||||
iconClassName: 'fa fa-fw fa-share',
|
||||
onClick: onSharePanel,
|
||||
shortcut: 'p s',
|
||||
});
|
||||
|
||||
const subMenu: PanelMenuItem[] = [];
|
||||
|
||||
if (!panel.fullscreen && dashboard.meta.canEdit) {
|
||||
subMenu.push({
|
||||
text: 'Duplicate',
|
||||
onClick: onDuplicatePanel,
|
||||
shortcut: 'p d',
|
||||
});
|
||||
|
||||
subMenu.push({
|
||||
text: 'Copy',
|
||||
onClick: onCopyPanel,
|
||||
});
|
||||
}
|
||||
|
||||
subMenu.push({
|
||||
text: 'Panel JSON',
|
||||
onClick: onEditPanelJson,
|
||||
});
|
||||
|
||||
menu.push({
|
||||
type: 'submenu',
|
||||
text: 'More...',
|
||||
iconClassName: 'fa fa-fw fa-cube',
|
||||
subMenu: subMenu,
|
||||
});
|
||||
|
||||
if (dashboard.meta.canEdit) {
|
||||
menu.push({ type: 'divider' });
|
||||
|
||||
menu.push({
|
||||
text: 'Remove',
|
||||
iconClassName: 'fa fa-fw fa-trash',
|
||||
onClick: onRemovePanel,
|
||||
shortcut: 'p r',
|
||||
});
|
||||
}
|
||||
|
||||
return menu;
|
||||
};
|
86
public/app/features/dashboard/utils/panel.ts
Normal file
86
public/app/features/dashboard/utils/panel.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { DashboardModel } from 'app/features/dashboard/dashboard_model';
|
||||
import { PanelModel } from 'app/features/dashboard/panel_model';
|
||||
import store from 'app/core/store';
|
||||
import { LS_PANEL_COPY_KEY } from 'app/core/constants';
|
||||
|
||||
export const removePanel = (dashboard: DashboardModel, panel: PanelModel, ask: boolean) => {
|
||||
// confirm deletion
|
||||
if (ask !== false) {
|
||||
const text2 = panel.alert ? 'Panel includes an alert rule, removing panel will also remove alert rule' : null;
|
||||
const confirmText = panel.alert ? 'YES' : null;
|
||||
|
||||
appEvents.emit('confirm-modal', {
|
||||
title: 'Remove Panel',
|
||||
text: 'Are you sure you want to remove this panel?',
|
||||
text2: text2,
|
||||
icon: 'fa-trash',
|
||||
confirmText: confirmText,
|
||||
yesText: 'Remove',
|
||||
onConfirm: () => removePanel(dashboard, panel, false),
|
||||
});
|
||||
return;
|
||||
}
|
||||
dashboard.removePanel(panel);
|
||||
};
|
||||
|
||||
export const duplicatePanel = (dashboard: DashboardModel, panel: PanelModel) => {
|
||||
dashboard.duplicatePanel(panel);
|
||||
};
|
||||
|
||||
export const copyPanel = (panel: PanelModel) => {
|
||||
store.set(LS_PANEL_COPY_KEY, JSON.stringify(panel.getSaveModel()));
|
||||
appEvents.emit('alert-success', ['Panel copied. Open Add Panel to paste']);
|
||||
};
|
||||
|
||||
const replacePanel = (dashboard: DashboardModel, newPanel: PanelModel, oldPanel: PanelModel) => {
|
||||
const index = dashboard.panels.findIndex(panel => {
|
||||
return panel.id === oldPanel.id;
|
||||
});
|
||||
|
||||
const deletedPanel = dashboard.panels.splice(index, 1);
|
||||
dashboard.events.emit('panel-removed', deletedPanel);
|
||||
|
||||
newPanel = new PanelModel(newPanel);
|
||||
newPanel.id = oldPanel.id;
|
||||
|
||||
dashboard.panels.splice(index, 0, newPanel);
|
||||
dashboard.sortPanelsByGridPos();
|
||||
dashboard.events.emit('panel-added', newPanel);
|
||||
};
|
||||
|
||||
export const editPanelJson = (dashboard: DashboardModel, panel: PanelModel) => {
|
||||
const model = {
|
||||
object: panel.getSaveModel(),
|
||||
updateHandler: (newPanel: PanelModel, oldPanel: PanelModel) => {
|
||||
replacePanel(dashboard, newPanel, oldPanel);
|
||||
},
|
||||
enableCopy: true,
|
||||
};
|
||||
|
||||
appEvents.emit('show-modal', {
|
||||
src: 'public/app/partials/edit_json.html',
|
||||
model: model,
|
||||
});
|
||||
};
|
||||
|
||||
export const sharePanel = (dashboard: DashboardModel, panel: PanelModel) => {
|
||||
appEvents.emit('show-modal', {
|
||||
src: 'public/app/features/dashboard/partials/shareModal.html',
|
||||
model: {
|
||||
dashboard: dashboard,
|
||||
panel: panel,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const refreshPanel = (panel: PanelModel) => {
|
||||
panel.refresh();
|
||||
};
|
||||
|
||||
export const toggleLegend = (panel: PanelModel) => {
|
||||
console.log('Toggle legend is not implemented yet');
|
||||
// We need to set panel.legend defaults first
|
||||
// panel.legend.show = !panel.legend.show;
|
||||
refreshPanel(panel);
|
||||
};
|
@ -1,11 +1,15 @@
|
||||
import config from 'app/core/config';
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import { appEvents, profiler } from 'app/core/core';
|
||||
import { PanelModel } from 'app/features/dashboard/panel_model';
|
||||
import { profiler } from 'app/core/core';
|
||||
import {
|
||||
duplicatePanel,
|
||||
copyPanel as copyPanelUtil,
|
||||
editPanelJson as editPanelJsonUtil,
|
||||
sharePanel as sharePanelUtil,
|
||||
} from 'app/features/dashboard/utils/panel';
|
||||
import Remarkable from 'remarkable';
|
||||
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, LS_PANEL_COPY_KEY } from 'app/core/constants';
|
||||
import store from 'app/core/store';
|
||||
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants';
|
||||
|
||||
const TITLE_HEIGHT = 27;
|
||||
const PANEL_BORDER = 2;
|
||||
@ -241,7 +245,7 @@ export class PanelCtrl {
|
||||
}
|
||||
|
||||
duplicate() {
|
||||
this.dashboard.duplicatePanel(this.panel);
|
||||
duplicatePanel(this.dashboard, this.panel);
|
||||
}
|
||||
|
||||
removePanel() {
|
||||
@ -251,48 +255,15 @@ export class PanelCtrl {
|
||||
}
|
||||
|
||||
editPanelJson() {
|
||||
const editScope = this.$scope.$root.$new();
|
||||
editScope.object = this.panel.getSaveModel();
|
||||
editScope.updateHandler = this.replacePanel.bind(this);
|
||||
editScope.enableCopy = true;
|
||||
|
||||
this.publishAppEvent('show-modal', {
|
||||
src: 'public/app/partials/edit_json.html',
|
||||
scope: editScope,
|
||||
});
|
||||
editPanelJsonUtil(this.dashboard, this.panel);
|
||||
}
|
||||
|
||||
copyPanel() {
|
||||
store.set(LS_PANEL_COPY_KEY, JSON.stringify(this.panel.getSaveModel()));
|
||||
appEvents.emit('alert-success', ['Panel copied. Open Add Panel to paste']);
|
||||
}
|
||||
|
||||
replacePanel(newPanel, oldPanel) {
|
||||
const dashboard = this.dashboard;
|
||||
const index = _.findIndex(dashboard.panels, panel => {
|
||||
return panel.id === oldPanel.id;
|
||||
});
|
||||
|
||||
const deletedPanel = dashboard.panels.splice(index, 1);
|
||||
this.dashboard.events.emit('panel-removed', deletedPanel);
|
||||
|
||||
newPanel = new PanelModel(newPanel);
|
||||
newPanel.id = oldPanel.id;
|
||||
|
||||
dashboard.panels.splice(index, 0, newPanel);
|
||||
dashboard.sortPanelsByGridPos();
|
||||
dashboard.events.emit('panel-added', newPanel);
|
||||
copyPanelUtil(this.panel);
|
||||
}
|
||||
|
||||
sharePanel() {
|
||||
const shareScope = this.$scope.$new();
|
||||
shareScope.panel = this.panel;
|
||||
shareScope.dashboard = this.dashboard;
|
||||
|
||||
this.publishAppEvent('show-modal', {
|
||||
src: 'public/app/features/dashboard/partials/shareModal.html',
|
||||
scope: shareScope,
|
||||
});
|
||||
sharePanelUtil(this.dashboard, this.panel);
|
||||
}
|
||||
|
||||
getInfoMode() {
|
||||
|
@ -16,9 +16,7 @@ export class VizTabCtrl {
|
||||
$scope.ctrl = this;
|
||||
}
|
||||
|
||||
onTypeChanged = (plugin: PanelPlugin) => {
|
||||
this.dashboard.changePanelType(this.panelCtrl.panel, plugin.id);
|
||||
};
|
||||
onTypeChanged = (plugin: PanelPlugin) => {};
|
||||
}
|
||||
|
||||
const template = `
|
||||
|
@ -20,7 +20,12 @@ export class ElasticConfigCtrl {
|
||||
{ name: 'Yearly', value: 'Yearly', example: '[logstash-]YYYY' },
|
||||
];
|
||||
|
||||
esVersions = [{ name: '2.x', value: 2 }, { name: '5.x', value: 5 }, { name: '5.6+', value: 56 }];
|
||||
esVersions = [
|
||||
{ name: '2.x', value: 2 },
|
||||
{ name: '5.x', value: 5 },
|
||||
{ name: '5.6+', value: 56 },
|
||||
{ name: '6.0+', value: 60 },
|
||||
];
|
||||
|
||||
indexPatternTypeChanged() {
|
||||
const def = _.find(this.indexPatternTypes, {
|
||||
|
@ -31,7 +31,11 @@ export class ElasticQueryBuilder {
|
||||
queryNode.terms.size = parseInt(aggDef.settings.size, 10) === 0 ? 500 : parseInt(aggDef.settings.size, 10);
|
||||
if (aggDef.settings.orderBy !== void 0) {
|
||||
queryNode.terms.order = {};
|
||||
queryNode.terms.order[aggDef.settings.orderBy] = aggDef.settings.order;
|
||||
if (aggDef.settings.orderBy === '_term' && this.esVersion >= 60) {
|
||||
queryNode.terms.order['_key'] = aggDef.settings.order;
|
||||
} else {
|
||||
queryNode.terms.order[aggDef.settings.orderBy] = aggDef.settings.order;
|
||||
}
|
||||
|
||||
// if metric ref, look it up and add it to this agg level
|
||||
metricRef = parseInt(aggDef.settings.orderBy, 10);
|
||||
@ -318,6 +322,13 @@ export class ElasticQueryBuilder {
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (this.esVersion >= 60) {
|
||||
query.aggs['1'].terms.order = {
|
||||
_key: 'asc',
|
||||
};
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
}
|
||||
|
@ -62,6 +62,54 @@ describe('ElasticQueryBuilder', () => {
|
||||
expect(aggs['1'].avg.field).toBe('@value');
|
||||
});
|
||||
|
||||
it('with term agg and order by term', () => {
|
||||
const query = builder.build(
|
||||
{
|
||||
metrics: [{ type: 'count', id: '1' }, { type: 'avg', field: '@value', id: '5' }],
|
||||
bucketAggs: [
|
||||
{
|
||||
type: 'terms',
|
||||
field: '@host',
|
||||
settings: { size: 5, order: 'asc', orderBy: '_term' },
|
||||
id: '2',
|
||||
},
|
||||
{ type: 'date_histogram', field: '@timestamp', id: '3' },
|
||||
],
|
||||
},
|
||||
100,
|
||||
1000
|
||||
);
|
||||
|
||||
const firstLevel = query.aggs['2'];
|
||||
expect(firstLevel.terms.order._term).toBe('asc');
|
||||
});
|
||||
|
||||
it('with term agg and order by term on es6.x', () => {
|
||||
const builder6x = new ElasticQueryBuilder({
|
||||
timeField: '@timestamp',
|
||||
esVersion: 60,
|
||||
});
|
||||
const query = builder6x.build(
|
||||
{
|
||||
metrics: [{ type: 'count', id: '1' }, { type: 'avg', field: '@value', id: '5' }],
|
||||
bucketAggs: [
|
||||
{
|
||||
type: 'terms',
|
||||
field: '@host',
|
||||
settings: { size: 5, order: 'asc', orderBy: '_term' },
|
||||
id: '2',
|
||||
},
|
||||
{ type: 'date_histogram', field: '@timestamp', id: '3' },
|
||||
],
|
||||
},
|
||||
100,
|
||||
1000
|
||||
);
|
||||
|
||||
const firstLevel = query.aggs['2'];
|
||||
expect(firstLevel.terms.order._key).toBe('asc');
|
||||
});
|
||||
|
||||
it('with term agg and order by metric agg', () => {
|
||||
const query = builder.build(
|
||||
{
|
||||
@ -302,4 +350,18 @@ describe('ElasticQueryBuilder', () => {
|
||||
expect(query.query.bool.filter[4].regexp['key5']).toBe('value5');
|
||||
expect(query.query.bool.filter[5].bool.must_not.regexp['key6']).toBe('value6');
|
||||
});
|
||||
|
||||
it('getTermsQuery should set correct sorting', () => {
|
||||
const query = builder.getTermsQuery({});
|
||||
expect(query.aggs['1'].terms.order._term).toBe('asc');
|
||||
});
|
||||
|
||||
it('getTermsQuery es6.x should set correct sorting', () => {
|
||||
const builder6x = new ElasticQueryBuilder({
|
||||
timeField: '@timestamp',
|
||||
esVersion: 60,
|
||||
});
|
||||
const query = builder6x.getTermsQuery({});
|
||||
expect(query.aggs['1'].terms.order._key).toBe('asc');
|
||||
});
|
||||
});
|
||||
|
@ -12,3 +12,12 @@ export interface PanelOptionsProps<T = any> {
|
||||
options: T;
|
||||
onChange: (options: T) => void;
|
||||
}
|
||||
|
||||
export interface PanelMenuItem {
|
||||
type?: 'submenu' | 'divider';
|
||||
text?: string;
|
||||
iconClassName?: string;
|
||||
onClick?: () => void;
|
||||
shortcut?: string;
|
||||
subMenu?: PanelMenuItem[];
|
||||
}
|
||||
|
@ -183,6 +183,11 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
& > .dropdown > .dropdown-menu {
|
||||
// Panel menu. TODO: See if we can merge this with above
|
||||
display: block;
|
||||
}
|
||||
|
||||
&.cascade-open {
|
||||
.dropdown-menu {
|
||||
display: block;
|
||||
|
@ -124,7 +124,6 @@ div.flot-text {
|
||||
padding: 3px 5px;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
left: 1px;
|
||||
|
@ -1,43 +0,0 @@
|
||||
FROM centos:6.6
|
||||
|
||||
RUN yum install -y yum-plugin-ovl initscripts curl tar gcc libc6-dev git gcc-c++ openssl-devel && \
|
||||
yum install -y g++ make automake autoconf curl-devel zlib-devel httpd-devel apr-devel apr-util-devel sqlite-devel && \
|
||||
yum install -y wget yum-utils bzip2 bzip2-devel && \
|
||||
yum install -y fontconfig freetype freetype-devel fontconfig-devel libstdc++ && \
|
||||
yum install -y rpm-build patch readline readline-devel libtool bison lzma && \
|
||||
yum install -y which tar
|
||||
|
||||
# Install RUBY 1.9.3
|
||||
# install necessary utilities
|
||||
# RUN yum install -y which tar
|
||||
RUN gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 && \
|
||||
curl -sSl https://raw.githubusercontent.com/rvm/rvm/master/binscripts/rvm-installer | bash -s stable && \
|
||||
source /etc/profile.d/rvm.sh && \
|
||||
/bin/bash -l -c "rvm requirements" && \
|
||||
/bin/bash -l -c "rvm install 2.1.9" && \
|
||||
/bin/bash -l -c "rvm use 2.1.9 --default"
|
||||
|
||||
# install nodejs
|
||||
RUN curl --silent --location https://rpm.nodesource.com/setup_6.x | bash - && \
|
||||
yum install -y nodejs --nogpgcheck
|
||||
|
||||
ENV GOLANG_VERSION 1.11
|
||||
|
||||
RUN wget https://dl.yarnpkg.com/rpm/yarn.repo -O /etc/yum.repos.d/yarn.repo && \
|
||||
yum install -y yarn --nogpgcheck && \
|
||||
wget https://storage.googleapis.com/golang/go${GOLANG_VERSION}.linux-amd64.tar.gz && \
|
||||
tar -C /usr/local -xzf go${GOLANG_VERSION}.linux-amd64.tar.gz
|
||||
|
||||
|
||||
ENV PATH /usr/local/go/bin:$PATH
|
||||
|
||||
RUN mkdir -p /go/src /go/bin && chmod -R 777 /go
|
||||
|
||||
ENV GOPATH /go
|
||||
ENV PATH /go/bin:$PATH
|
||||
|
||||
ADD ./build.sh /tmp/
|
||||
|
||||
WORKDIR /tmp/
|
||||
|
||||
CMD ["./build.sh"]
|
@ -1,17 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
docker info && docker version
|
||||
mkdir -p ~/docker
|
||||
|
||||
echo "Circle branch: ${CIRCLE_BRANCH}"
|
||||
echo "Circle tag: ${CIRCLE_TAG}"
|
||||
|
||||
# try to load docker container from cache
|
||||
if [[ -e ~/docker/centos.tar ]]; then
|
||||
docker load -i ~/docker/centos.tar;
|
||||
else
|
||||
docker build --rm=false --tag "grafana/buildcontainer" ./scripts/build/
|
||||
|
||||
# save docker container so we don't have to recreate it next run
|
||||
docker save grafana/buildcontainer > ~/docker/centos.tar;
|
||||
fi
|
Loading…
Reference in New Issue
Block a user