From 10d706dccff98ff5504bd49094bf5004de88c365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 18 Oct 2018 14:34:25 +0200 Subject: [PATCH 01/57] wip: enterprise docs --- docs/sources/enterprise/index.md | 11 +++++++++++ docs/sources/whatsnew/index.md | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 docs/sources/enterprise/index.md diff --git a/docs/sources/enterprise/index.md b/docs/sources/enterprise/index.md new file mode 100644 index 00000000000..a241d84b50e --- /dev/null +++ b/docs/sources/enterprise/index.md @@ -0,0 +1,11 @@ ++++ +title = "Grafana Enterprise" +description = "Grafana Enterprise overview" +type = "docs" +[menu.docs] +name = "Enterprise" +identifier = "enterprise" +weight = 4 ++++ + +### Grafana Enterprise diff --git a/docs/sources/whatsnew/index.md b/docs/sources/whatsnew/index.md index df472f07093..f4159643d72 100644 --- a/docs/sources/whatsnew/index.md +++ b/docs/sources/whatsnew/index.md @@ -3,7 +3,7 @@ title = "What's New in Grafana" [menu.docs] name = "What's New In Grafana" identifier = "whatsnew" -weight = 3 +weight = 5 +++ From a8e2840f15e9957b28cdde280530376f4552fa69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 30 Oct 2018 15:25:10 +0100 Subject: [PATCH 02/57] minor progress --- docs/sources/enterprise/index.md | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/docs/sources/enterprise/index.md b/docs/sources/enterprise/index.md index a241d84b50e..3583064f9c3 100644 --- a/docs/sources/enterprise/index.md +++ b/docs/sources/enterprise/index.md @@ -3,9 +3,30 @@ title = "Grafana Enterprise" description = "Grafana Enterprise overview" type = "docs" [menu.docs] -name = "Enterprise" +name = "Grafana Enterprise" identifier = "enterprise" -weight = 4 +weight = 5 +++ -### Grafana Enterprise +# Grafana Enterprise + +Grafana Enterprise is a commercial edition of Grafana that includes additional features not found in the open source +version. + +## Enterprise features + +Grafana Enterprise includes all of the features found in the open source version. Below we list the additional features +that can only be found in the Enterprise edition. + +### Enhanced LDAP + +With Grafana Enterprise you can setup syncing between LDAP Groups and Teams. [Learn More](link). + +### Data source permissions + +Assign and restrict query permissions on Data Sources to specific teams or users. [Learn More](link). + +## Try Grafana Enterprise + +## Licence file mangement + From 621525d10fa636e51c932c364c6f08fcc96e5a32 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 30 Oct 2018 18:43:54 +0100 Subject: [PATCH 03/57] restructure administration/permissions page into a section with sub pages --- docs/sources/administration/permissions.md | 116 ------------------ docs/sources/enterprise/index.md | 4 +- .../dashboard_folder_permissions.md | 67 ++++++++++ .../permissions/datasource_permissions.md | 71 +++++++++++ docs/sources/permissions/index.md | 12 ++ .../sources/permissions/organization_roles.md | 38 ++++++ docs/sources/permissions/overview.md | 42 +++++++ 7 files changed, 232 insertions(+), 118 deletions(-) delete mode 100644 docs/sources/administration/permissions.md create mode 100644 docs/sources/permissions/dashboard_folder_permissions.md create mode 100644 docs/sources/permissions/datasource_permissions.md create mode 100644 docs/sources/permissions/index.md create mode 100644 docs/sources/permissions/organization_roles.md create mode 100644 docs/sources/permissions/overview.md diff --git a/docs/sources/administration/permissions.md b/docs/sources/administration/permissions.md deleted file mode 100644 index 0d374f03647..00000000000 --- a/docs/sources/administration/permissions.md +++ /dev/null @@ -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. - diff --git a/docs/sources/enterprise/index.md b/docs/sources/enterprise/index.md index 3583064f9c3..378de9d6371 100644 --- a/docs/sources/enterprise/index.md +++ b/docs/sources/enterprise/index.md @@ -22,9 +22,9 @@ that can only be found in the Enterprise edition. With Grafana Enterprise you can setup syncing between LDAP Groups and Teams. [Learn More](link). -### Data source permissions +### Datasource Permissions -Assign and restrict query permissions on Data Sources to specific teams or users. [Learn More](link). +Datasource permissions allows you to restrict access for users to query a datasource. [Learn More]({{< relref "permissions/datasource_permissions.md" >}}). ## Try Grafana Enterprise diff --git a/docs/sources/permissions/dashboard_folder_permissions.md b/docs/sources/permissions/dashboard_folder_permissions.md new file mode 100644 index 00000000000..fb82f00d712 --- /dev/null +++ b/docs/sources/permissions/dashboard_folder_permissions.md @@ -0,0 +1,67 @@ ++++ +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 Permissions" +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. 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. diff --git a/docs/sources/permissions/datasource_permissions.md b/docs/sources/permissions/datasource_permissions.md new file mode 100644 index 00000000000..fd5405fd684 --- /dev/null +++ b/docs/sources/permissions/datasource_permissions.md @@ -0,0 +1,71 @@ ++++ +title = "Datasource Permissions" +description = "Grafana Datasource Permissions Guide " +keywords = ["grafana", "configuration", "documentation", "datasource", "permissions", "users", "teams"] +type = "docs" +[menu.docs] +name = "Datasource Permissions" +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 add 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. + +
+ +## 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. + +
+ +## 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 revoke 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. + +
diff --git a/docs/sources/permissions/index.md b/docs/sources/permissions/index.md new file mode 100644 index 00000000000..42514f76baf --- /dev/null +++ b/docs/sources/permissions/index.md @@ -0,0 +1,12 @@ ++++ +title = "Permissions" +description = "Permissions" +type = "docs" +[menu.docs] +name = "Permissions" +identifier = "permissions" +parent = "admin" +weight = 3 ++++ + + diff --git a/docs/sources/permissions/organization_roles.md b/docs/sources/permissions/organization_roles.md new file mode 100644 index 00000000000..626d79fad87 --- /dev/null +++ b/docs/sources/permissions/organization_roles.md @@ -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. diff --git a/docs/sources/permissions/overview.md b/docs/sources/permissions/overview.md new file mode 100644 index 00000000000..cd3fc5417b6 --- /dev/null +++ b/docs/sources/permissions/overview.md @@ -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" >}}). From fc6d7c9b6b041ffddb07df3cb6b02dc2fd299a0a Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 30 Oct 2018 19:02:12 +0100 Subject: [PATCH 04/57] datasource permission http api --- .../http_api/datasource_permissions.md | 249 ++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 docs/sources/http_api/datasource_permissions.md diff --git a/docs/sources/http_api/datasource_permissions.md b/docs/sources/http_api/datasource_permissions.md new file mode 100644 index 00000000000..aa4d498ef85 --- /dev/null +++ b/docs/sources/http_api/datasource_permissions.md @@ -0,0 +1,249 @@ ++++ +title = "Datasource Permissions HTTP API " +description = "Grafana Datasource Permissions HTTP API" +keywords = ["grafana", "http", "documentation", "api", "datasource", "permission", "permissions", "acl"] +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 a permission have been added which permits 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 From 5495072c83ef872bbc3b797efb02d05811547b24 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 31 Oct 2018 17:17:19 +0100 Subject: [PATCH 05/57] docs: fix datasource permissions keywords --- docs/sources/http_api/datasource_permissions.md | 2 +- docs/sources/permissions/datasource_permissions.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/http_api/datasource_permissions.md b/docs/sources/http_api/datasource_permissions.md index aa4d498ef85..bc193113b43 100644 --- a/docs/sources/http_api/datasource_permissions.md +++ b/docs/sources/http_api/datasource_permissions.md @@ -1,7 +1,7 @@ +++ title = "Datasource Permissions HTTP API " description = "Grafana Datasource Permissions HTTP API" -keywords = ["grafana", "http", "documentation", "api", "datasource", "permission", "permissions", "acl"] +keywords = ["grafana", "http", "documentation", "api", "datasource", "permission", "permissions", "acl", "enterprise"] aliases = ["/http_api/datasourcepermissions/"] type = "docs" [menu.docs] diff --git a/docs/sources/permissions/datasource_permissions.md b/docs/sources/permissions/datasource_permissions.md index fd5405fd684..f1cbd31b85f 100644 --- a/docs/sources/permissions/datasource_permissions.md +++ b/docs/sources/permissions/datasource_permissions.md @@ -1,7 +1,7 @@ +++ title = "Datasource Permissions" description = "Grafana Datasource Permissions Guide " -keywords = ["grafana", "configuration", "documentation", "datasource", "permissions", "users", "teams"] +keywords = ["grafana", "configuration", "documentation", "datasource", "permissions", "users", "teams", "enterprise"] type = "docs" [menu.docs] name = "Datasource Permissions" From 280c8631f924c570fe933c0f7293336732989e28 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 31 Oct 2018 18:01:30 +0100 Subject: [PATCH 06/57] docs: enhanced ldap --- docs/sources/auth/enhanced_ldap.md | 43 +++++++ docs/sources/enterprise/index.md | 7 +- docs/sources/http_api/external_group_sync.md | 111 +++++++++++++++++++ 3 files changed, 158 insertions(+), 3 deletions(-) create mode 100644 docs/sources/auth/enhanced_ldap.md create mode 100644 docs/sources/http_api/external_group_sync.md diff --git a/docs/sources/auth/enhanced_ldap.md b/docs/sources/auth/enhanced_ldap.md new file mode 100644 index 00000000000..8eec57b1429 --- /dev/null +++ b/docs/sources/auth/enhanced_ldap.md @@ -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. + +
+ +### 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. + +
diff --git a/docs/sources/enterprise/index.md b/docs/sources/enterprise/index.md index 378de9d6371..97a06f1ab47 100644 --- a/docs/sources/enterprise/index.md +++ b/docs/sources/enterprise/index.md @@ -1,6 +1,7 @@ +++ title = "Grafana Enterprise" description = "Grafana Enterprise overview" +keywords = ["grafana", "documentation", "datasource", "permissions", "ldap", "licensing", "enterprise"] type = "docs" [menu.docs] name = "Grafana Enterprise" @@ -18,9 +19,9 @@ version. Grafana Enterprise includes all of the features found in the open source version. Below we list the additional features that can only be found in the Enterprise edition. -### Enhanced LDAP +### Enhanced LDAP Integration -With Grafana Enterprise you can setup syncing between LDAP Groups and Teams. [Learn More](link). +With Grafana Enterprise you can setup synchronization between LDAP Groups and Teams. [Learn More]({{< relref "auth/enhanced_ldap.md" >}}). ### Datasource Permissions @@ -28,5 +29,5 @@ Datasource permissions allows you to restrict access for users to query a dataso ## Try Grafana Enterprise -## Licence file mangement +## Licence file management diff --git a/docs/sources/http_api/external_group_sync.md b/docs/sources/http_api/external_group_sync.md new file mode 100644 index 00000000000..2ce06c2c94e --- /dev/null +++ b/docs/sources/http_api/external_group_sync.md @@ -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 From a1b4ebc11516046b94b6b1bfed3a9fe4f834ba2b Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 1 Nov 2018 11:00:32 +0100 Subject: [PATCH 07/57] make permission sub items in sidemenu cleaner --- docs/sources/permissions/dashboard_folder_permissions.md | 2 +- docs/sources/permissions/datasource_permissions.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/permissions/dashboard_folder_permissions.md b/docs/sources/permissions/dashboard_folder_permissions.md index fb82f00d712..aed0f91ee7c 100644 --- a/docs/sources/permissions/dashboard_folder_permissions.md +++ b/docs/sources/permissions/dashboard_folder_permissions.md @@ -4,7 +4,7 @@ description = "Grafana Dashboard & Folder Permissions Guide " keywords = ["grafana", "configuration", "documentation", "dashboard", "folder", "permissions", "teams"] type = "docs" [menu.docs] -name = "Dashboard & Folder Permissions" +name = "Dashboard & Folder" identifier = "dashboard-folder-permissions" parent = "permissions" weight = 3 diff --git a/docs/sources/permissions/datasource_permissions.md b/docs/sources/permissions/datasource_permissions.md index f1cbd31b85f..f94fc47c4d2 100644 --- a/docs/sources/permissions/datasource_permissions.md +++ b/docs/sources/permissions/datasource_permissions.md @@ -4,7 +4,7 @@ description = "Grafana Datasource Permissions Guide " keywords = ["grafana", "configuration", "documentation", "datasource", "permissions", "users", "teams", "enterprise"] type = "docs" [menu.docs] -name = "Datasource Permissions" +name = "Datasource" identifier = "datasource-permissions" parent = "permissions" weight = 4 From 5a27df2dc96c76766a178627048a03d0a3b08365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 1 Nov 2018 12:17:04 +0100 Subject: [PATCH 08/57] updated enterprise page --- docs/sources/enterprise/index.md | 39 +++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/docs/sources/enterprise/index.md b/docs/sources/enterprise/index.md index 97a06f1ab47..d0e7798d3d8 100644 --- a/docs/sources/enterprise/index.md +++ b/docs/sources/enterprise/index.md @@ -14,7 +14,8 @@ weight = 5 Grafana Enterprise is a commercial edition of Grafana that includes additional features not found in the open source version. -## Enterprise features +Building on everything you already know and love about Grafana, Grafana Enterprise layers on [premium data sources](https://grafana.com/plugins?premium=1), + advanced authentication options, **Data Source** permissions and 24x7x365 support and training from the core Grafana team. Grafana Enterprise includes all of the features found in the open source version. Below we list the additional features that can only be found in the Enterprise edition. @@ -25,9 +26,41 @@ With Grafana Enterprise you can setup synchronization between LDAP Groups and Te ### Datasource Permissions -Datasource permissions allows you to restrict access for users to query a datasource. [Learn More]({{< relref "permissions/datasource_permissions.md" >}}). +Datasource permissions allows 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 these premium plugins. + +* [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 -## Licence file management +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 login to you [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 your 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. + + From 4c070bc781662064e4702c674f6f40d841f88ce2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 1 Nov 2018 12:35:51 +0100 Subject: [PATCH 09/57] minor doc tweaks --- docs/sources/enterprise/index.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/sources/enterprise/index.md b/docs/sources/enterprise/index.md index d0e7798d3d8..6d5f77ab6c8 100644 --- a/docs/sources/enterprise/index.md +++ b/docs/sources/enterprise/index.md @@ -14,11 +14,11 @@ weight = 5 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 layers on [premium data sources](https://grafana.com/plugins?premium=1), - advanced authentication options, **Data Source** permissions and 24x7x365 support and training from the core Grafana team. +Building on everything you already know and love about Grafana, Grafana Enterprise layers on premium data sources. +advanced authentication options, more permissions controls and 24x7x365 support and training from the core Grafana team. -Grafana Enterprise includes all of the features found in the open source version. Below we list the additional features -that can only be found in the Enterprise edition. +Grafana Enterprise includes all of the features found in the open source edition. Below we list the additional features +that can only be found in the Grafana Enterprise. ### Enhanced LDAP Integration From 5803bfd2c78e702750b87923c571eadbe0fa8499 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 2 Nov 2018 17:52:40 +0100 Subject: [PATCH 10/57] fix terms agg order deprecation warning on es 6+ --- docs/sources/administration/provisioning.md | 2 +- .../features/datasources/elasticsearch.md | 2 +- .../elasticsearch/client/search_request.go | 25 +++++--- .../elasticsearch/time_series_query_test.go | 54 ++++++++++++++++ .../datasource/elasticsearch/config_ctrl.ts | 7 ++- .../datasource/elasticsearch/query_builder.ts | 13 +++- .../elasticsearch/specs/query_builder.test.ts | 62 +++++++++++++++++++ 7 files changed, 154 insertions(+), 11 deletions(-) diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md index 9149aa42130..60e89b486a5 100644 --- a/docs/sources/administration/provisioning.md +++ b/docs/sources/administration/provisioning.md @@ -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 | diff --git a/docs/sources/features/datasources/elasticsearch.md b/docs/sources/features/datasources/elasticsearch.md index 80a2f9a828a..aa60eb7cbc1 100644 --- a/docs/sources/features/datasources/elasticsearch.md +++ b/docs/sources/features/datasources/elasticsearch.md @@ -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. diff --git a/pkg/tsdb/elasticsearch/client/search_request.go b/pkg/tsdb/elasticsearch/client/search_request.go index 4c577a2c31d..d89a98cbadb 100644 --- a/pkg/tsdb/elasticsearch/client/search_request.go +++ b/pkg/tsdb/elasticsearch/client/search_request.go @@ -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) } diff --git a/pkg/tsdb/elasticsearch/time_series_query_test.go b/pkg/tsdb/elasticsearch/time_series_query_test.go index fe8ae0fa8f2..9660d70c318 100644 --- a/pkg/tsdb/elasticsearch/time_series_query_test.go +++ b/pkg/tsdb/elasticsearch/time_series_query_test.go @@ -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, `{ diff --git a/public/app/plugins/datasource/elasticsearch/config_ctrl.ts b/public/app/plugins/datasource/elasticsearch/config_ctrl.ts index b872cc090c1..154ff9bcb91 100644 --- a/public/app/plugins/datasource/elasticsearch/config_ctrl.ts +++ b/public/app/plugins/datasource/elasticsearch/config_ctrl.ts @@ -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, { diff --git a/public/app/plugins/datasource/elasticsearch/query_builder.ts b/public/app/plugins/datasource/elasticsearch/query_builder.ts index a4d92397d80..21af0ba9b80 100644 --- a/public/app/plugins/datasource/elasticsearch/query_builder.ts +++ b/public/app/plugins/datasource/elasticsearch/query_builder.ts @@ -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; } } diff --git a/public/app/plugins/datasource/elasticsearch/specs/query_builder.test.ts b/public/app/plugins/datasource/elasticsearch/specs/query_builder.test.ts index a9e570f366b..84929e83003 100644 --- a/public/app/plugins/datasource/elasticsearch/specs/query_builder.test.ts +++ b/public/app/plugins/datasource/elasticsearch/specs/query_builder.test.ts @@ -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'); + }); }); From 17adb58d803008562faaaa6ff51546816154e773 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 5 Nov 2018 17:32:28 +0100 Subject: [PATCH 11/57] export: provide more help regarding export format this will provide the user with more info about the export format and default to not use the format for sharing on grafana.com etc. ref #13781 --- .../dashboard/export/export_modal.html | 12 +++++- .../features/dashboard/export/export_modal.ts | 38 ++++++++++++++----- 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/public/app/features/dashboard/export/export_modal.html b/public/app/features/dashboard/export/export_modal.html index 0598c612fd6..3505e50b821 100644 --- a/public/app/features/dashboard/export/export_modal.html +++ b/public/app/features/dashboard/export/export_modal.html @@ -15,11 +15,19 @@ You can share dashboards on Grafana.com

+ + +
- - Cancel diff --git a/public/app/features/dashboard/export/export_modal.ts b/public/app/features/dashboard/export/export_modal.ts index f99946915d6..08a79702ed5 100644 --- a/public/app/features/dashboard/export/export_modal.ts +++ b/public/app/features/dashboard/export/export_modal.ts @@ -8,27 +8,47 @@ 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(); + } + + saveDashboardAsFile() { + if (this.shareExternally) { + this.exporter.makeExportable(this.dash).then((dashboardJson: any) => { + this.$scope.$apply(() => { + this._saveFile(dashboardJson); + }); }); - }); + } else { + this._saveFile(this.dash.getSaveModelClone()); + } + } + + viewJson() { + if (this.shareExternally) { + this.exporter.makeExportable(this.dash).then((dashboardJson: any) => { + this.$scope.$apply(() => { + this._viewJson(dashboardJson); + }); + }); + } else { + this._viewJson(this.dash.getSaveModelClone()); + } } - save() { - const blob = new Blob([angular.toJson(this.dash, true)], { + _saveFile(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; + _viewJson(clone: any) { const editScope = this.$rootScope.$new(); editScope.object = clone; editScope.enableCopy = true; From 7bde98aff9789c071bb3221d50ffe17798e371bb Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 6 Nov 2018 09:00:17 +0100 Subject: [PATCH 12/57] rename and mark functions as private --- public/app/features/dashboard/export/export_modal.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/public/app/features/dashboard/export/export_modal.ts b/public/app/features/dashboard/export/export_modal.ts index 08a79702ed5..0e48041ca87 100644 --- a/public/app/features/dashboard/export/export_modal.ts +++ b/public/app/features/dashboard/export/export_modal.ts @@ -21,11 +21,11 @@ export class DashExportCtrl { if (this.shareExternally) { this.exporter.makeExportable(this.dash).then((dashboardJson: any) => { this.$scope.$apply(() => { - this._saveFile(dashboardJson); + this.openSaveAsDialog(dashboardJson); }); }); } else { - this._saveFile(this.dash.getSaveModelClone()); + this.openSaveAsDialog(this.dash.getSaveModelClone()); } } @@ -33,22 +33,22 @@ export class DashExportCtrl { if (this.shareExternally) { this.exporter.makeExportable(this.dash).then((dashboardJson: any) => { this.$scope.$apply(() => { - this._viewJson(dashboardJson); + this.openJsonModal(dashboardJson); }); }); } else { - this._viewJson(this.dash.getSaveModelClone()); + this.openJsonModal(this.dash.getSaveModelClone()); } } - _saveFile(dash: any) { + private openSaveAsDialog(dash: any) { const blob = new Blob([angular.toJson(dash, true)], { type: 'application/json;charset=utf-8', }); saveAs(blob, dash.title + '-' + new Date().getTime() + '.json'); } - _viewJson(clone: any) { + private openJsonModal(clone: any) { const editScope = this.$rootScope.$new(); editScope.object = clone; editScope.enableCopy = true; From 9f6683de2c67bf1db3af798c9146cbbfeb321c66 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Thu, 25 Oct 2018 12:47:09 +0200 Subject: [PATCH 13/57] wip: Initial commit for PanelHeaderMenu --- .../features/dashboard/dashgrid/DataPanel.tsx | 1 - .../dashboard/dashgrid/PanelChrome.tsx | 2 +- .../dashboard/dashgrid/PanelHeader.tsx | 83 ------------- .../dashgrid/PanelHeader/PanelHeader.tsx | 50 ++++++++ .../dashgrid/PanelHeader/PanelHeaderMenu.tsx | 109 ++++++++++++++++++ .../PanelHeader/PanelHeaderMenuItem.tsx | 34 ++++++ public/sass/components/_dropdown.scss | 5 + public/sass/pages/_dashboard.scss | 1 - 8 files changed, 199 insertions(+), 86 deletions(-) delete mode 100644 public/app/features/dashboard/dashgrid/PanelHeader.tsx create mode 100644 public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx create mode 100644 public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx create mode 100644 public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem.tsx diff --git a/public/app/features/dashboard/dashgrid/DataPanel.tsx b/public/app/features/dashboard/dashgrid/DataPanel.tsx index d0122363668..a42d392c018 100644 --- a/public/app/features/dashboard/dashgrid/DataPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DataPanel.tsx @@ -38,7 +38,6 @@ export class DataPanel extends Component { constructor(props: Props) { super(props); - this.state = { loading: LoadingState.NotStarted, response: { diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 953dfd62368..d4bfce67c48 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -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 diff --git a/public/app/features/dashboard/dashgrid/PanelHeader.tsx b/public/app/features/dashboard/dashgrid/PanelHeader.tsx deleted file mode 100644 index 12d5cd37253..00000000000 --- a/public/app/features/dashboard/dashgrid/PanelHeader.tsx +++ /dev/null @@ -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 { - 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 ( -
- - - - - - {isLoading && ( - - - - )} - -
- - - {this.props.panel.title} - - - - - - 4m - - -
-
- ); - } -} diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx new file mode 100644 index 00000000000..a5e30d9396e --- /dev/null +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import classNames from 'classnames'; +import { PanelModel } from 'app/features/dashboard/panel_model'; +import { DashboardModel } from 'app/features/dashboard/dashboard_model'; +// import { store } from 'app/store/configureStore'; +// import { updateLocation } from 'app/core/actions'; +import { PanelHeaderMenu } from './PanelHeaderMenu'; +// import appEvents from 'app/core/app_events'; + +interface PanelHeaderProps { + panel: PanelModel; + dashboard: DashboardModel; +} + +export class PanelHeader extends React.Component { + render() { + const isFullscreen = false; + const isLoading = false; + const panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen }); + + return ( +
+ + + + + + {isLoading && ( + + + + )} + +
+
+ + + {this.props.panel.title} + + + + + 4m + +
+
+
+ ); + } +} diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx new file mode 100644 index 00000000000..6bc6bb54509 --- /dev/null +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx @@ -0,0 +1,109 @@ +import React, { PureComponent } from 'react'; +// import { store } from 'app/store/configureStore'; +import { PanelHeaderMenuItem, PanelHeaderMenuItemTypes } from './PanelHeaderMenuItem'; +import appEvents from 'app/core/app_events'; +import { store } from 'app/store/configureStore'; +import { updateLocation } from 'app/core/actions'; + +export interface PanelHeaderMenuProps { + panelId: number; +} + +export class PanelHeaderMenu extends PureComponent { + onEditPanel = () => { + store.dispatch( + updateLocation({ + query: { + panelId: this.props.panelId, + edit: true, + fullscreen: true, + }, + }) + ); + }; + + onViewPanel = () => { + store.dispatch( + updateLocation({ + query: { + panelId: this.props.panelId, + edit: false, + fullscreen: true, + }, + }) + ); + }; + + onRemovePanel = () => { + appEvents.emit('panel-remove', { + panelId: this.props.panelId, + }); + }; + + render() { + return ( +
+
    + + + {}} + shortcut="p s" + /> + {}} + > +
      + {}} + shortcut="p d" + /> + + {}} /> + + {}} /> + + {}} /> + + {}} + shortcut="p l" + /> +
    +
    + + +
+
+ ); + } +} diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem.tsx new file mode 100644 index 00000000000..3eb4e72ca9d --- /dev/null +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem.tsx @@ -0,0 +1,34 @@ +import React, { SFC } from 'react'; + +export enum PanelHeaderMenuItemTypes { + Button = 'Button', // ? + Divider = 'Divider', + Link = 'Link', + SubMenu = 'SubMenu', +} + +export interface PanelHeaderMenuItemProps { + type: PanelHeaderMenuItemTypes; + text?: string; + iconClassName?: string; + handleClick?: () => void; + shortcut?: string; + children?: any; +} + +export const PanelHeaderMenuItem: SFC = props => { + const isSubMenu = props.type === PanelHeaderMenuItemTypes.SubMenu; + const isDivider = props.type === PanelHeaderMenuItemTypes.Divider; + return isDivider ? ( +
  • + ) : ( +
  • + + {props.iconClassName && } + {props.text} + {props.shortcut && {props.shortcut}} + + {props.children} +
  • + ); +}; diff --git a/public/sass/components/_dropdown.scss b/public/sass/components/_dropdown.scss index 37dbdcd89ef..9e7f46fe514 100644 --- a/public/sass/components/_dropdown.scss +++ b/public/sass/components/_dropdown.scss @@ -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; diff --git a/public/sass/pages/_dashboard.scss b/public/sass/pages/_dashboard.scss index 795766a22de..125edac500f 100644 --- a/public/sass/pages/_dashboard.scss +++ b/public/sass/pages/_dashboard.scss @@ -138,7 +138,6 @@ div.flot-text { padding: 3px 5px; visibility: hidden; opacity: 0; - position: absolute; width: 16px; height: 16px; left: 1px; From 212c086162c0f3f55e7eba10216c769c6a051e47 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Thu, 25 Oct 2018 13:57:23 +0200 Subject: [PATCH 14/57] Mobx is now Redux --- public/app/core/services/bridge_srv.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/core/services/bridge_srv.ts b/public/app/core/services/bridge_srv.ts index ee184c243ac..1c91673495d 100644 --- a/public/app/core/services/bridge_srv.ts +++ b/public/app/core/services/bridge_srv.ts @@ -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; From 820e47b4c0519f5bf1b8b926bef288080f7c0400 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Thu, 25 Oct 2018 13:58:26 +0200 Subject: [PATCH 15/57] wip: panel-header: Remove panel --- .../dashgrid/PanelHeader/PanelHeader.tsx | 3 +- .../dashgrid/PanelHeader/PanelHeaderMenu.tsx | 32 +++++++++++++++++-- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx index a5e30d9396e..ae04f0f0405 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx @@ -14,6 +14,7 @@ interface PanelHeaderProps { export class PanelHeader extends React.Component { render() { + const { dashboard } = this.props; const isFullscreen = false; const isLoading = false; const panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen }); @@ -38,7 +39,7 @@ export class PanelHeader extends React.Component { {this.props.panel.title} - + 4m diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx index 6bc6bb54509..19fee872e4b 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx @@ -1,5 +1,7 @@ import React, { PureComponent } from 'react'; // import { store } from 'app/store/configureStore'; +import { DashboardModel } from 'app/features/dashboard/dashboard_model'; +import { PanelModel } from 'app/features/dashboard/panel_model'; import { PanelHeaderMenuItem, PanelHeaderMenuItemTypes } from './PanelHeaderMenuItem'; import appEvents from 'app/core/app_events'; import { store } from 'app/store/configureStore'; @@ -7,6 +9,7 @@ import { updateLocation } from 'app/core/actions'; export interface PanelHeaderMenuProps { panelId: number; + dashboard: DashboardModel; } export class PanelHeaderMenu extends PureComponent { @@ -35,9 +38,32 @@ export class PanelHeaderMenu extends PureComponent { }; onRemovePanel = () => { - appEvents.emit('panel-remove', { - panelId: this.props.panelId, - }); + const { panelId, dashboard } = this.props; + const panelInfo = dashboard.getPanelInfoById(panelId); + this.removePanel(panelInfo.panel, true); + }; + + removePanel = (panel: PanelModel, ask: boolean) => { + const { dashboard } = this.props; + + // 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: () => this.removePanel(panel, false), + }); + return; + } + + dashboard.removePanel(panel); }; render() { From bf8703edb88664e15f6244d53a778f7fc6bff5f2 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Thu, 25 Oct 2018 14:29:03 +0200 Subject: [PATCH 16/57] wip: panel-header: Move code existing in both angular+react to utility functions --- .../app/features/dashboard/dashboard_ctrl.ts | 31 ++--------------- .../dashgrid/PanelHeader/PanelHeaderMenu.tsx | 33 ++----------------- public/app/features/dashboard/utils/panel.ts | 27 +++++++++++++++ 3 files changed, 31 insertions(+), 60 deletions(-) create mode 100644 public/app/features/dashboard/utils/panel.ts diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts index 5871a579f3c..60517df19f6 100644 --- a/public/app/features/dashboard/dashboard_ctrl.ts +++ b/public/app/features/dashboard/dashboard_ctrl.ts @@ -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; @@ -136,34 +136,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() { diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx index 19fee872e4b..3a27796bb90 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx @@ -1,11 +1,9 @@ import React, { PureComponent } from 'react'; -// import { store } from 'app/store/configureStore'; import { DashboardModel } from 'app/features/dashboard/dashboard_model'; -import { PanelModel } from 'app/features/dashboard/panel_model'; import { PanelHeaderMenuItem, PanelHeaderMenuItemTypes } from './PanelHeaderMenuItem'; -import appEvents from 'app/core/app_events'; import { store } from 'app/store/configureStore'; import { updateLocation } from 'app/core/actions'; +import { removePanel } from 'app/features/dashboard/utils/panel'; export interface PanelHeaderMenuProps { panelId: number; @@ -40,30 +38,7 @@ export class PanelHeaderMenu extends PureComponent { onRemovePanel = () => { const { panelId, dashboard } = this.props; const panelInfo = dashboard.getPanelInfoById(panelId); - this.removePanel(panelInfo.panel, true); - }; - - removePanel = (panel: PanelModel, ask: boolean) => { - const { dashboard } = this.props; - - // 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: () => this.removePanel(panel, false), - }); - return; - } - - dashboard.removePanel(panel); + removePanel(dashboard, panelInfo.panel, true); }; render() { @@ -105,13 +80,9 @@ export class PanelHeaderMenu extends PureComponent { handleClick={() => {}} shortcut="p d" /> - {}} /> - {}} /> - {}} /> - { + // 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 default { + removePanel, +}; From 839057dc7a393667b231b42fb4d7451a0db77782 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 30 Oct 2018 14:21:47 +0100 Subject: [PATCH 17/57] wip: Add "Share" to the react panels --- .../dashgrid/PanelHeader/PanelHeaderMenu.tsx | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx index 3a27796bb90..5e6f5a5d28c 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx @@ -4,6 +4,7 @@ import { PanelHeaderMenuItem, PanelHeaderMenuItemTypes } from './PanelHeaderMenu import { store } from 'app/store/configureStore'; import { updateLocation } from 'app/core/actions'; import { removePanel } from 'app/features/dashboard/utils/panel'; +import appEvents from 'app/core/app_events'; export interface PanelHeaderMenuProps { panelId: number; @@ -11,6 +12,13 @@ export interface PanelHeaderMenuProps { } export class PanelHeaderMenu extends PureComponent { + getPanel = () => { + // Pass in panel as prop instead? + const { panelId, dashboard } = this.props; + const panelInfo = dashboard.getPanelInfoById(panelId); + return panelInfo.panel; + }; + onEditPanel = () => { store.dispatch( updateLocation({ @@ -36,9 +44,22 @@ export class PanelHeaderMenu extends PureComponent { }; onRemovePanel = () => { - const { panelId, dashboard } = this.props; - const panelInfo = dashboard.getPanelInfoById(panelId); - removePanel(dashboard, panelInfo.panel, true); + const { dashboard } = this.props; + const panel = this.getPanel(); + removePanel(dashboard, panel, true); + }; + + onSharePanel = () => { + const { dashboard } = this.props; + const panel = this.getPanel(); + + appEvents.emit('show-modal', { + src: 'public/app/features/dashboard/partials/shareModal.html', + model: { + panel: panel, + dashboard: dashboard, + }, + }); }; render() { @@ -63,7 +84,7 @@ export class PanelHeaderMenu extends PureComponent { type={PanelHeaderMenuItemTypes.Link} text="Share" iconClassName="fa fa-fw fa-share" - handleClick={() => {}} + handleClick={this.onSharePanel} shortcut="p s" /> Date: Tue, 30 Oct 2018 14:38:18 +0100 Subject: [PATCH 18/57] wip: panel-header: Add "Duplicate" --- .../dashgrid/PanelHeader/PanelHeaderMenu.tsx | 11 +++++++++-- public/app/features/dashboard/utils/panel.ts | 5 +++++ public/app/features/panel/panel_ctrl.ts | 3 ++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx index 5e6f5a5d28c..49adfbb703a 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx @@ -3,7 +3,7 @@ import { DashboardModel } from 'app/features/dashboard/dashboard_model'; import { PanelHeaderMenuItem, PanelHeaderMenuItemTypes } from './PanelHeaderMenuItem'; import { store } from 'app/store/configureStore'; import { updateLocation } from 'app/core/actions'; -import { removePanel } from 'app/features/dashboard/utils/panel'; +import { removePanel, duplicatePanel } from 'app/features/dashboard/utils/panel'; import appEvents from 'app/core/app_events'; export interface PanelHeaderMenuProps { @@ -62,6 +62,13 @@ export class PanelHeaderMenu extends PureComponent { }); }; + onDuplicatePanel = () => { + const { dashboard } = this.props; + const panel = this.getPanel(); + + duplicatePanel(dashboard, panel); + }; + render() { return (
    @@ -98,7 +105,7 @@ export class PanelHeaderMenu extends PureComponent { type={PanelHeaderMenuItemTypes.Link} text="Duplicate" iconClassName="" - handleClick={() => {}} + handleClick={this.onDuplicatePanel} shortcut="p d" /> {}} /> diff --git a/public/app/features/dashboard/utils/panel.ts b/public/app/features/dashboard/utils/panel.ts index 6257aae232e..35ccfdf3b3f 100644 --- a/public/app/features/dashboard/utils/panel.ts +++ b/public/app/features/dashboard/utils/panel.ts @@ -22,6 +22,11 @@ export const removePanel = (dashboard: DashboardModel, panel: PanelModel, ask: b dashboard.removePanel(panel); }; +export const duplicatePanel = (dashboard: DashboardModel, panel: PanelModel) => { + dashboard.duplicatePanel(panel); +}; + export default { removePanel, + duplicatePanel, }; diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index 08605132e82..d1e2dcc20cf 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -3,6 +3,7 @@ import _ from 'lodash'; import $ from 'jquery'; import { appEvents, profiler } from 'app/core/core'; import { PanelModel } from 'app/features/dashboard/panel_model'; +import { duplicatePanel } 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'; @@ -241,7 +242,7 @@ export class PanelCtrl { } duplicate() { - this.dashboard.duplicatePanel(this.panel); + duplicatePanel(this.dashboard, this.panel); } removePanel() { From edceb204e7a552b1ebd82149b76b78de96474cec Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 30 Oct 2018 14:53:04 +0100 Subject: [PATCH 19/57] wip: panel-header: Add "Copy" functionality --- .../dashgrid/PanelHeader/PanelHeaderMenu.tsx | 11 ++++++++--- public/app/features/dashboard/utils/panel.ts | 8 ++++++++ public/app/features/panel/panel_ctrl.ts | 10 ++++------ 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx index 49adfbb703a..190c13ead9d 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx @@ -3,7 +3,7 @@ import { DashboardModel } from 'app/features/dashboard/dashboard_model'; import { PanelHeaderMenuItem, PanelHeaderMenuItemTypes } from './PanelHeaderMenuItem'; import { store } from 'app/store/configureStore'; import { updateLocation } from 'app/core/actions'; -import { removePanel, duplicatePanel } from 'app/features/dashboard/utils/panel'; +import { removePanel, duplicatePanel, copyPanel } from 'app/features/dashboard/utils/panel'; import appEvents from 'app/core/app_events'; export interface PanelHeaderMenuProps { @@ -69,6 +69,11 @@ export class PanelHeaderMenu extends PureComponent { duplicatePanel(dashboard, panel); }; + onCopyPanel = () => { + const panel = this.getPanel(); + copyPanel(panel); + }; + render() { return (
    @@ -98,7 +103,7 @@ export class PanelHeaderMenu extends PureComponent { type={PanelHeaderMenuItemTypes.SubMenu} text="More ..." iconClassName="fa fa-fw fa-cube" - handleClick={() => {}} + handleClick={null} >
      { handleClick={this.onDuplicatePanel} shortcut="p d" /> - {}} /> + {}} /> {}} /> { // confirm deletion @@ -26,7 +28,13 @@ 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']); +}; + export default { removePanel, duplicatePanel, + copyPanel, }; diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index d1e2dcc20cf..cb35b5ef470 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -1,12 +1,11 @@ import config from 'app/core/config'; import _ from 'lodash'; import $ from 'jquery'; -import { appEvents, profiler } from 'app/core/core'; +import { profiler } from 'app/core/core'; import { PanelModel } from 'app/features/dashboard/panel_model'; -import { duplicatePanel } from 'app/features/dashboard/utils/panel'; +import { duplicatePanel, copyPanel } 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; @@ -264,8 +263,7 @@ export class PanelCtrl { } copyPanel() { - store.set(LS_PANEL_COPY_KEY, JSON.stringify(this.panel.getSaveModel())); - appEvents.emit('alert-success', ['Panel copied. Open Add Panel to paste']); + copyPanel(this.panel); } replacePanel(newPanel, oldPanel) { From f9dd5165782e9841f464dfaf3fa1205ebaa7b7cc Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 30 Oct 2018 16:07:59 +0100 Subject: [PATCH 20/57] wip: panel-header: Add "Edit JSON" functionality + make sure everyone using the json editor pass in the model property instead of the scope property when triggering the json modal --- .../app/core/controllers/json_editor_ctrl.ts | 8 ++--- .../app/features/dashboard/dashboard_ctrl.ts | 11 +++--- .../dashgrid/PanelHeader/PanelHeaderMenu.tsx | 14 ++++++-- .../features/dashboard/export/export_modal.ts | 9 ++--- public/app/features/dashboard/utils/panel.ts | 33 ++++++++++++++--- public/app/features/panel/panel_ctrl.ts | 36 ++++--------------- 6 files changed, 63 insertions(+), 48 deletions(-) diff --git a/public/app/core/controllers/json_editor_ctrl.ts b/public/app/core/controllers/json_editor_ctrl.ts index 9c3f9d9e98d..7439433c55e 100644 --- a/public/app/core/controllers/json_editor_ctrl.ts +++ b/public/app/core/controllers/json_editor_ctrl.ts @@ -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; diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts index 60517df19f6..6611a728803 100644 --- a/public/app/features/dashboard/dashboard_ctrl.ts +++ b/public/app/features/dashboard/dashboard_ctrl.ts @@ -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, }); } diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx index 190c13ead9d..9ac1e91483c 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx @@ -3,7 +3,7 @@ import { DashboardModel } from 'app/features/dashboard/dashboard_model'; import { PanelHeaderMenuItem, PanelHeaderMenuItemTypes } from './PanelHeaderMenuItem'; import { store } from 'app/store/configureStore'; import { updateLocation } from 'app/core/actions'; -import { removePanel, duplicatePanel, copyPanel } from 'app/features/dashboard/utils/panel'; +import { removePanel, duplicatePanel, copyPanel, editPanelJson } from 'app/features/dashboard/utils/panel'; import appEvents from 'app/core/app_events'; export interface PanelHeaderMenuProps { @@ -74,6 +74,12 @@ export class PanelHeaderMenu extends PureComponent { copyPanel(panel); }; + onEditPanelJson = () => { + const { dashboard } = this.props; + const panel = this.getPanel(); + editPanelJson(dashboard, panel); + }; + render() { return (
      @@ -114,7 +120,11 @@ export class PanelHeaderMenu extends PureComponent { shortcut="p d" /> - {}} /> + {}} /> { appEvents.emit('alert-success', ['Panel copied. Open Add Panel to paste']); }; -export default { - removePanel, - duplicatePanel, - copyPanel, +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, + }); }; diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index cb35b5ef470..169ec8b322b 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -2,8 +2,11 @@ import config from 'app/core/config'; import _ from 'lodash'; import $ from 'jquery'; import { profiler } from 'app/core/core'; -import { PanelModel } from 'app/features/dashboard/panel_model'; -import { duplicatePanel, copyPanel } from 'app/features/dashboard/utils/panel'; +import { + duplicatePanel, + copyPanel as copyPanelUtil, + editPanelJson as editPanelJsonUtil, +} from 'app/features/dashboard/utils/panel'; import Remarkable from 'remarkable'; import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants'; @@ -251,36 +254,11 @@ 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() { - copyPanel(this.panel); - } - - 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() { From 5375ce5ffdcc7b6588edcdc9c06c0ad8c5563f71 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 30 Oct 2018 16:39:08 +0100 Subject: [PATCH 21/57] wip: panel-header: Refactor so "Share" use the same code in angular+react --- .../dashgrid/PanelHeader/PanelHeaderMenu.tsx | 11 ++--------- public/app/features/dashboard/shareModalCtrl.ts | 2 ++ public/app/features/dashboard/utils/panel.ts | 10 ++++++++++ public/app/features/panel/panel_ctrl.ts | 10 ++-------- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx index 9ac1e91483c..13b0fca86e9 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx @@ -3,8 +3,7 @@ import { DashboardModel } from 'app/features/dashboard/dashboard_model'; import { PanelHeaderMenuItem, PanelHeaderMenuItemTypes } from './PanelHeaderMenuItem'; import { store } from 'app/store/configureStore'; import { updateLocation } from 'app/core/actions'; -import { removePanel, duplicatePanel, copyPanel, editPanelJson } from 'app/features/dashboard/utils/panel'; -import appEvents from 'app/core/app_events'; +import { removePanel, duplicatePanel, copyPanel, editPanelJson, sharePanel } from 'app/features/dashboard/utils/panel'; export interface PanelHeaderMenuProps { panelId: number; @@ -53,13 +52,7 @@ export class PanelHeaderMenu extends PureComponent { const { dashboard } = this.props; const panel = this.getPanel(); - appEvents.emit('show-modal', { - src: 'public/app/features/dashboard/partials/shareModal.html', - model: { - panel: panel, - dashboard: dashboard, - }, - }); + sharePanel(dashboard, panel); }; onDuplicatePanel = () => { diff --git a/public/app/features/dashboard/shareModalCtrl.ts b/public/app/features/dashboard/shareModalCtrl.ts index c00a6d8d57f..f894d24202f 100644 --- a/public/app/features/dashboard/shareModalCtrl.ts +++ b/public/app/features/dashboard/shareModalCtrl.ts @@ -12,6 +12,8 @@ export function ShareModalCtrl($scope, $rootScope, $location, $timeout, timeSrv, $scope.editor = { index: $scope.tabIndex || 0 }; $scope.init = () => { + $scope.panel = $scope.model.panel || $scope.panel; // React pass panel and dashboard in the "model" property + $scope.dashboard = $scope.model.dashboard || $scope.dashboard; $scope.modeSharePanel = $scope.panel ? true : false; $scope.tabs = [{ title: 'Link', src: 'shareLink.html' }]; diff --git a/public/app/features/dashboard/utils/panel.ts b/public/app/features/dashboard/utils/panel.ts index 7f89090c336..fff837d9685 100644 --- a/public/app/features/dashboard/utils/panel.ts +++ b/public/app/features/dashboard/utils/panel.ts @@ -63,3 +63,13 @@ export const editPanelJson = (dashboard: DashboardModel, panel: PanelModel) => { 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, + }, + }); +}; diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index 169ec8b322b..92932142690 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -6,6 +6,7 @@ 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 } from 'app/core/constants'; @@ -262,14 +263,7 @@ export class PanelCtrl { } 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() { From 79da3dc9f6f89576727fc966a615051b1663c25c Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 31 Oct 2018 13:41:50 +0100 Subject: [PATCH 22/57] wip: panel-header: Change DashboardPanel to a PureComponent to avoid unwanted rerenders --- public/app/features/dashboard/dashgrid/DashboardPanel.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index 7dd8a06996d..fcfc84e287b 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { PureComponent } from 'react'; import config from 'app/core/config'; import { PanelModel } from '../panel_model'; import { DashboardModel } from '../dashboard_model'; @@ -20,7 +20,7 @@ export interface State { pluginExports: PluginExports; } -export class DashboardPanel extends React.Component { +export class DashboardPanel extends PureComponent { element: any; angularPanel: AngularComponent; pluginInfo: any; From 61513102169f05d7955d34254830c85cd302cf01 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 31 Oct 2018 13:43:21 +0100 Subject: [PATCH 23/57] wip: panel-header: Start implementing the Toggle legend, but its not taken all the way --- .../dashgrid/PanelHeader/PanelHeaderMenu.tsx | 16 ++++++++++++++-- public/app/features/dashboard/utils/panel.ts | 11 +++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx index 13b0fca86e9..826406fddff 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx @@ -3,7 +3,14 @@ import { DashboardModel } from 'app/features/dashboard/dashboard_model'; import { PanelHeaderMenuItem, PanelHeaderMenuItemTypes } from './PanelHeaderMenuItem'; import { store } from 'app/store/configureStore'; import { updateLocation } from 'app/core/actions'; -import { removePanel, duplicatePanel, copyPanel, editPanelJson, sharePanel } from 'app/features/dashboard/utils/panel'; +import { + removePanel, + duplicatePanel, + copyPanel, + editPanelJson, + sharePanel, + toggleLegend, +} from 'app/features/dashboard/utils/panel'; export interface PanelHeaderMenuProps { panelId: number; @@ -73,6 +80,11 @@ export class PanelHeaderMenu extends PureComponent { editPanelJson(dashboard, panel); }; + onToggleLegend = () => { + const panel = this.getPanel(); + toggleLegend(panel); + }; + render() { return (
      @@ -122,7 +134,7 @@ export class PanelHeaderMenu extends PureComponent { {}} + handleClick={this.onToggleLegend} shortcut="p l" />
    diff --git a/public/app/features/dashboard/utils/panel.ts b/public/app/features/dashboard/utils/panel.ts index fff837d9685..151c1ea8d61 100644 --- a/public/app/features/dashboard/utils/panel.ts +++ b/public/app/features/dashboard/utils/panel.ts @@ -73,3 +73,14 @@ export const sharePanel = (dashboard: DashboardModel, panel: PanelModel) => { }, }); }; + +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); +}; From f124b9de6a0c12125d420c25f7e2625901b05af6 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 31 Oct 2018 16:04:14 +0100 Subject: [PATCH 24/57] wip: panel-header: Separate all panel actions to its own file so we decouple them from react --- .../dashgrid/PanelHeader/PanelHeaderMenu.tsx | 159 +++--------------- .../PanelHeader/PanelHeaderMenuItem.tsx | 4 +- .../features/dashboard/utils/panel_menu.ts | 140 +++++++++++++++ 3 files changed, 169 insertions(+), 134 deletions(-) create mode 100644 public/app/features/dashboard/utils/panel_menu.ts diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx index 826406fddff..adce83e8c40 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx @@ -1,16 +1,7 @@ -import React, { PureComponent } from 'react'; +import React, { PureComponent, Fragment } from 'react'; import { DashboardModel } from 'app/features/dashboard/dashboard_model'; -import { PanelHeaderMenuItem, PanelHeaderMenuItemTypes } from './PanelHeaderMenuItem'; -import { store } from 'app/store/configureStore'; -import { updateLocation } from 'app/core/actions'; -import { - removePanel, - duplicatePanel, - copyPanel, - editPanelJson, - sharePanel, - toggleLegend, -} from 'app/features/dashboard/utils/panel'; +import { PanelHeaderMenuItem, PanelHeaderMenuItemProps } from './PanelHeaderMenuItem'; +import { getPanelMenu } from 'app/features/dashboard/utils/panel_menu'; export interface PanelHeaderMenuProps { panelId: number; @@ -25,130 +16,32 @@ export class PanelHeaderMenu extends PureComponent { return panelInfo.panel; }; - onEditPanel = () => { - store.dispatch( - updateLocation({ - query: { - panelId: this.props.panelId, - edit: true, - fullscreen: true, - }, - }) - ); - }; - - onViewPanel = () => { - store.dispatch( - updateLocation({ - query: { - panelId: this.props.panelId, - edit: false, - fullscreen: true, - }, - }) + renderItems = (menu: PanelHeaderMenuItemProps[], isSubMenu = false) => { + return ( +
      + {menu.map(menuItem => { + console.log(this); + return ( + + + {menuItem.subMenu && this.renderItems(menuItem.subMenu, true)} + + + ); + })} +
    ); }; - onRemovePanel = () => { - const { dashboard } = this.props; - const panel = this.getPanel(); - removePanel(dashboard, panel, true); - }; - - onSharePanel = () => { - const { dashboard } = this.props; - const panel = this.getPanel(); - - sharePanel(dashboard, panel); - }; - - onDuplicatePanel = () => { - const { dashboard } = this.props; - const panel = this.getPanel(); - - duplicatePanel(dashboard, panel); - }; - - onCopyPanel = () => { - const panel = this.getPanel(); - copyPanel(panel); - }; - - onEditPanelJson = () => { - const { dashboard } = this.props; - const panel = this.getPanel(); - editPanelJson(dashboard, panel); - }; - - onToggleLegend = () => { - const panel = this.getPanel(); - toggleLegend(panel); - }; - render() { - return ( -
    -
      - - - - -
        - - - - {}} /> - -
      -
      - - -
    -
    - ); + const { dashboard } = this.props; + const menu = getPanelMenu(dashboard, this.getPanel()); + return
    {this.renderItems(menu)}
    ; } } diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem.tsx index 3eb4e72ca9d..f0b5579c2a1 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem.tsx @@ -1,6 +1,6 @@ import React, { SFC } from 'react'; -export enum PanelHeaderMenuItemTypes { +export enum PanelHeaderMenuItemTypes { // TODO: Evaluate. Remove? Button = 'Button', // ? Divider = 'Divider', Link = 'Link', @@ -14,6 +14,8 @@ export interface PanelHeaderMenuItemProps { handleClick?: () => void; shortcut?: string; children?: any; + subMenu?: PanelHeaderMenuItemProps[]; + role?: string; } export const PanelHeaderMenuItem: SFC = props => { diff --git a/public/app/features/dashboard/utils/panel_menu.ts b/public/app/features/dashboard/utils/panel_menu.ts new file mode 100644 index 00000000000..de2ba852d13 --- /dev/null +++ b/public/app/features/dashboard/utils/panel_menu.ts @@ -0,0 +1,140 @@ +import { PanelHeaderMenuItemTypes, PanelHeaderMenuItemProps } from './../dashgrid/PanelHeader/PanelHeaderMenuItem'; +import { store } from 'app/store/configureStore'; +import { updateLocation } from 'app/core/actions'; +import { PanelModel } from 'app/features/dashboard/panel_model'; +import { DashboardModel } from 'app/features/dashboard/dashboard_model'; +import { removePanel, duplicatePanel, copyPanel, editPanelJson, sharePanel } from 'app/features/dashboard/utils/panel'; + +export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => { + const onViewPanel = () => { + store.dispatch( + updateLocation({ + query: { + panelId: panel.id, + edit: false, + fullscreen: true, + }, + }) + ); + }; + + const onEditPanel = () => { + store.dispatch( + updateLocation({ + query: { + panelId: panel.id, + edit: true, + fullscreen: 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 getSubMenu = () => { + const menu: PanelHeaderMenuItemProps[] = []; + + if (!panel.fullscreen && dashboard.meta.canEdit) { + menu.push({ + type: PanelHeaderMenuItemTypes.Link, + text: 'Duplicate', + handleClick: onDuplicatePanel, + shortcut: 'p d', + role: 'Editor', + }); + menu.push({ + type: PanelHeaderMenuItemTypes.Link, + text: 'Copy', + handleClick: onCopyPanel, + role: 'Editor', + }); + } + + menu.push({ + type: PanelHeaderMenuItemTypes.Link, + text: 'Panel JSON', + handleClick: onEditPanelJson, + }); + + // TODO: Handle this somehow + // this.events.emit('init-panel-actions', menu); + return menu; + }; + + const menu: PanelHeaderMenuItemProps[] = []; + + menu.push({ + type: PanelHeaderMenuItemTypes.Link, + text: 'View', + iconClassName: 'fa fa-fw fa-eye', + handleClick: onViewPanel, + shortcut: 'v', + }); + + if (dashboard.meta.canEdit) { + menu.push({ + type: PanelHeaderMenuItemTypes.Link, + text: 'Edit', + iconClassName: 'fa fa-fw fa-edit', + handleClick: onEditPanel, + shortcut: 'e', + role: 'Editor', + }); + } + + menu.push({ + type: PanelHeaderMenuItemTypes.Link, + text: 'Share', + iconClassName: 'fa fa-fw fa-share', + handleClick: onSharePanel, + shortcut: 'p s', + }); + + const subMenu: PanelHeaderMenuItemProps[] = getSubMenu(); + + menu.push({ + type: PanelHeaderMenuItemTypes.SubMenu, + text: 'More...', + iconClassName: 'fa fa-fw fa-cube', + handleClick: null, + subMenu: subMenu, + }); + + if (dashboard.meta.canEdit) { + menu.push({ + type: PanelHeaderMenuItemTypes.Divider, + role: 'Editor', + }); + menu.push({ + type: PanelHeaderMenuItemTypes.Link, + text: 'Remove', + iconClassName: 'fa fa-fw fa-trash', + handleClick: onRemovePanel, + shortcut: 'p r', + role: 'Editor', + }); + } + + // Additional items from sub-class + // menu.push(...this.getAdditionalMenuItems()); + return menu; +}; From 443d381dd91444d81fbf614828fd0c6b06f52b7c Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 31 Oct 2018 16:19:11 +0100 Subject: [PATCH 25/57] wip: panel-header: Add possibility to add custom actions to the menu by passing them in as props --- public/app/features/dashboard/utils/panel_menu.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/public/app/features/dashboard/utils/panel_menu.ts b/public/app/features/dashboard/utils/panel_menu.ts index de2ba852d13..c86595953b5 100644 --- a/public/app/features/dashboard/utils/panel_menu.ts +++ b/public/app/features/dashboard/utils/panel_menu.ts @@ -5,7 +5,12 @@ import { PanelModel } from 'app/features/dashboard/panel_model'; import { DashboardModel } from 'app/features/dashboard/dashboard_model'; import { removePanel, duplicatePanel, copyPanel, editPanelJson, sharePanel } from 'app/features/dashboard/utils/panel'; -export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => { +export const getPanelMenu = ( + dashboard: DashboardModel, + panel: PanelModel, + extraMenuItems: PanelHeaderMenuItemProps[] = [], + extraSubMenuItems: PanelHeaderMenuItemProps[] = [] +) => { const onViewPanel = () => { store.dispatch( updateLocation({ @@ -77,6 +82,9 @@ export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => { // TODO: Handle this somehow // this.events.emit('init-panel-actions', menu); + extraSubMenuItems.forEach(item => { + menu.push(item); + }); return menu; }; @@ -109,6 +117,10 @@ export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => { shortcut: 'p s', }); + extraMenuItems.forEach(item => { + menu.push(item); + }); + const subMenu: PanelHeaderMenuItemProps[] = getSubMenu(); menu.push({ From f471482569a3d391c8c76d8ef6019f9251da9244 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 31 Oct 2018 16:22:48 +0100 Subject: [PATCH 26/57] wip: panel-header: Fragment not needed anymore --- .../dashgrid/PanelHeader/PanelHeaderMenu.tsx | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx index adce83e8c40..b454ccad4a6 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx @@ -1,4 +1,4 @@ -import React, { PureComponent, Fragment } from 'react'; +import React, { PureComponent } from 'react'; import { DashboardModel } from 'app/features/dashboard/dashboard_model'; import { PanelHeaderMenuItem, PanelHeaderMenuItemProps } from './PanelHeaderMenuItem'; import { getPanelMenu } from 'app/features/dashboard/utils/panel_menu'; @@ -22,17 +22,15 @@ export class PanelHeaderMenu extends PureComponent { {menu.map(menuItem => { console.log(this); return ( - - - {menuItem.subMenu && this.renderItems(menuItem.subMenu, true)} - - + + {menuItem.subMenu && this.renderItems(menuItem.subMenu, true)} + ); })} From ca4612af261aec49970ad45ca57a2e46e482cb62 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Thu, 1 Nov 2018 12:01:27 +0100 Subject: [PATCH 27/57] wip: panel-header: Merge conflicts --- .../dashboard/dashgrid/DashboardPanel.tsx | 3 +- .../dashboard/dashgrid/PanelChrome.tsx | 12 +-- .../dashgrid/PanelHeader/PanelHeader.tsx | 14 ++- .../dashgrid/PanelHeader/PanelHeaderMenu.tsx | 12 ++- .../features/dashboard/utils/panel_menu.ts | 10 +- public/app/plugins/panel/graph2/module.tsx | 1 + .../plugins/panel/graph2/withMenuOptions.tsx | 94 +++++++++++++++++++ public/app/types/plugins.ts | 1 + 8 files changed, 122 insertions(+), 25 deletions(-) create mode 100644 public/app/plugins/panel/graph2/withMenuOptions.tsx diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index fcfc84e287b..cf41595ce8c 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -1,4 +1,4 @@ -import React, { PureComponent } from 'react'; +import React, { PureComponent } from 'react'; import config from 'app/core/config'; import { PanelModel } from '../panel_model'; import { DashboardModel } from '../dashboard_model'; @@ -123,6 +123,7 @@ export class DashboardPanel extends PureComponent {
    diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index d4bfce67c48..1a6f5a3cee2 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -13,19 +13,20 @@ import { PanelModel } from '../panel_model'; import { DashboardModel } from '../dashboard_model'; import { TimeRange, PanelProps } from 'app/types'; -export interface Props { +export interface PanelChromeProps { panel: PanelModel; dashboard: DashboardModel; component: ComponentClass; + withMenuOptions: any; } -export interface State { +export interface PanelChromeState { refreshCounter: number; renderCounter: number; timeRange?: TimeRange; } -export class PanelChrome extends PureComponent { +export class PanelChrome extends PureComponent { constructor(props) { super(props); @@ -67,16 +68,15 @@ export class PanelChrome extends PureComponent { } render() { - const { panel, dashboard } = this.props; + const { panel, dashboard, withMenuOptions } = this.props; const { datasource, targets } = panel; const { timeRange, renderCounter, refreshCounter } = this.state; const PanelComponent = this.props.component; - console.log('Panel chrome render'); return (
    - +
    { +export class PanelHeader extends PureComponent { render() { - const { dashboard } = this.props; + const { dashboard, withMenuOptions, panel } = this.props; const isFullscreen = false; const isLoading = false; const panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen }); + const PanelHeaderMenuComponent = withMenuOptions ? withMenuOptions(PanelHeaderMenu, panel) : PanelHeaderMenu; return (
    @@ -39,7 +37,7 @@ export class PanelHeader extends React.Component { {this.props.panel.title} - + 4m diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx index b454ccad4a6..c36eb9d8584 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx @@ -6,6 +6,9 @@ import { getPanelMenu } from 'app/features/dashboard/utils/panel_menu'; export interface PanelHeaderMenuProps { panelId: number; dashboard: DashboardModel; + datasource: any; + additionalMenuItems?: PanelHeaderMenuItemProps[]; + additionalSubMenuItems?: PanelHeaderMenuItemProps[]; } export class PanelHeaderMenu extends PureComponent { @@ -19,10 +22,10 @@ export class PanelHeaderMenu extends PureComponent { renderItems = (menu: PanelHeaderMenuItemProps[], isSubMenu = false) => { return (
      - {menu.map(menuItem => { - console.log(this); + {menu.map((menuItem, idx) => { return ( { }; render() { - const { dashboard } = this.props; - const menu = getPanelMenu(dashboard, this.getPanel()); + console.log('PanelHeaderMenu render'); + const { dashboard, additionalMenuItems, additionalSubMenuItems } = this.props; + const menu = getPanelMenu(dashboard, this.getPanel(), additionalMenuItems, additionalSubMenuItems); return
      {this.renderItems(menu)}
      ; } } diff --git a/public/app/features/dashboard/utils/panel_menu.ts b/public/app/features/dashboard/utils/panel_menu.ts index c86595953b5..67adf118edd 100644 --- a/public/app/features/dashboard/utils/panel_menu.ts +++ b/public/app/features/dashboard/utils/panel_menu.ts @@ -8,8 +8,8 @@ import { removePanel, duplicatePanel, copyPanel, editPanelJson, sharePanel } fro export const getPanelMenu = ( dashboard: DashboardModel, panel: PanelModel, - extraMenuItems: PanelHeaderMenuItemProps[] = [], - extraSubMenuItems: PanelHeaderMenuItemProps[] = [] + additionalMenuItems: PanelHeaderMenuItemProps[] = [], + additionalSubMenuItems: PanelHeaderMenuItemProps[] = [] ) => { const onViewPanel = () => { store.dispatch( @@ -80,9 +80,7 @@ export const getPanelMenu = ( handleClick: onEditPanelJson, }); - // TODO: Handle this somehow - // this.events.emit('init-panel-actions', menu); - extraSubMenuItems.forEach(item => { + additionalSubMenuItems.forEach(item => { menu.push(item); }); return menu; @@ -117,7 +115,7 @@ export const getPanelMenu = ( shortcut: 'p s', }); - extraMenuItems.forEach(item => { + additionalMenuItems.forEach(item => { menu.push(item); }); diff --git a/public/app/plugins/panel/graph2/module.tsx b/public/app/plugins/panel/graph2/module.tsx index b132d3374f1..88b679e1645 100644 --- a/public/app/plugins/panel/graph2/module.tsx +++ b/public/app/plugins/panel/graph2/module.tsx @@ -73,3 +73,4 @@ export class GraphOptions extends PureComponent> { } export { Graph2 as PanelComponent, GraphOptions as PanelOptionsComponent }; +export { withMenuOptions } from './withMenuOptions'; diff --git a/public/app/plugins/panel/graph2/withMenuOptions.tsx b/public/app/plugins/panel/graph2/withMenuOptions.tsx new file mode 100644 index 00000000000..aaa89bf3406 --- /dev/null +++ b/public/app/plugins/panel/graph2/withMenuOptions.tsx @@ -0,0 +1,94 @@ +// Libraries +import React, { PureComponent } from 'react'; + +// Services +import { getTimeSrv } from 'app/features/dashboard/time_srv'; +import { contextSrv } from 'app/core/services/context_srv'; +import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; +import { store } from 'app/store/configureStore'; + +// Components +import { PanelHeaderMenu } from 'app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu'; +import config from 'app/core/config'; +import { getExploreUrl } from 'app/core/utils/explore'; +import { updateLocation } from 'app/core/actions'; + +// Types +import { PanelModel } from 'app/features/dashboard/panel_model'; +import { PanelHeaderMenuProps } from 'app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu'; +import { + PanelHeaderMenuItemProps, + PanelHeaderMenuItemTypes, +} from 'app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem'; + +interface LocalState { + datasource: any; +} + +export const withMenuOptions = (WrappedPanelHeaderMenu: typeof PanelHeaderMenu, panel: PanelModel) => { + return class extends PureComponent { + private datasourceSrv = getDatasourceSrv(); + private timeSrv = getTimeSrv(); + + constructor(props) { + super(props); + this.state = { + datasource: undefined, + }; + } + + componentDidMount() { + const dsPromise = getDatasourceSrv().get(panel.datasource); + dsPromise.then((datasource: any) => { + this.setState(() => ({ datasource })); + }); + } + + onExploreClick = async () => { + const { datasource } = this.state; + const url = await getExploreUrl(panel, panel.targets, datasource, this.datasourceSrv, this.timeSrv); + if (url) { + store.dispatch(updateLocation({ path: url })); + } + }; + + getAdditionalMenuItems = () => { + const { datasource } = this.state; + const items = []; + if ( + config.exploreEnabled && + contextSrv.isEditor && + datasource && + (datasource.meta.explore || datasource.meta.id === 'mixed') + ) { + items.push({ + type: PanelHeaderMenuItemTypes.Link, + text: 'Explore', + handleClick: this.onExploreClick, + iconClassName: 'fa fa-fw fa-rocket', + shortcut: 'x', + }); + } + return items; + }; + + getAdditionalSubMenuItems = () => { + return [ + { + type: PanelHeaderMenuItemTypes.Link, + text: 'Hello Sub Menu', + handleClick: () => { + alert('Hello world from HOC!'); + }, + shortcut: 's h w', + }, + ] as PanelHeaderMenuItemProps[]; + }; + + render() { + const menu: PanelHeaderMenuItemProps[] = this.getAdditionalMenuItems(); + const subMenu: PanelHeaderMenuItemProps[] = this.getAdditionalSubMenuItems(); + return ; + } + }; +}; diff --git a/public/app/types/plugins.ts b/public/app/types/plugins.ts index 817777669d8..9ede3dd9f4b 100644 --- a/public/app/types/plugins.ts +++ b/public/app/types/plugins.ts @@ -13,6 +13,7 @@ export interface PluginExports { PanelCtrl?; PanelComponent?: ComponentClass; PanelOptionsComponent: ComponentClass; + withMenuOptions?: any; } export interface PanelPlugin { From 6c0c1254fe65e098bec784cc74508845d61648fa Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Sat, 3 Nov 2018 23:36:40 +0100 Subject: [PATCH 28/57] wip: panel-header: More merge conflicts --- .../dashboard/dashgrid/DashboardPanel.tsx | 2 +- .../features/dashboard/dashgrid/DataPanel.tsx | 26 ++++--- .../dashboard/dashgrid/PanelChrome.tsx | 65 +++++++++++++--- .../dashgrid/PanelHeader/PanelHeader.tsx | 15 ++-- .../dashgrid/PanelHeader/PanelHeaderMenu.tsx | 21 +++-- .../features/dashboard/utils/panel_menu.ts | 16 ++-- public/app/plugins/panel/graph2/module.tsx | 3 + .../app/plugins/panel/graph2/moduleMenu.tsx | 76 +++++++++++++++++++ public/app/types/plugins.ts | 1 + 9 files changed, 174 insertions(+), 51 deletions(-) create mode 100644 public/app/plugins/panel/graph2/moduleMenu.tsx diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index cf41595ce8c..d75e3abc67f 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -115,7 +115,6 @@ export class DashboardPanel extends PureComponent { const { pluginExports } = this.state; const containerClass = this.props.panel.isEditing ? 'panel-editor-container' : 'panel-height-helper'; const panelWrapperClass = this.props.panel.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 ( @@ -126,6 +125,7 @@ export class DashboardPanel extends PureComponent { withMenuOptions={pluginExports.withMenuOptions} panel={this.props.panel} dashboard={this.props.dashboard} + moduleMenu={pluginExports.moduleMenu} />
    {this.props.panel.isEditing && ( diff --git a/public/app/features/dashboard/dashgrid/DataPanel.tsx b/public/app/features/dashboard/dashgrid/DataPanel.tsx index a42d392c018..77460d9dc83 100644 --- a/public/app/features/dashboard/dashgrid/DataPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DataPanel.tsx @@ -1,11 +1,9 @@ // Library import React, { Component } from 'react'; -// Services -import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; - // Types import { TimeRange, LoadingState, DataQueryOptions, DataQueryResponse, TimeSeries } from 'app/types'; +import { DataSourceApi } from 'app/types/series'; interface RenderProps { loading: LoadingState; @@ -13,7 +11,7 @@ interface RenderProps { } export interface Props { - datasource: string | null; + dataSourceApi: DataSourceApi; queries: any[]; panelId?: number; dashboardId?: number; @@ -21,6 +19,7 @@ export interface Props { timeRange?: TimeRange; refreshCounter: number; children: (r: RenderProps) => JSX.Element; + onIssueQueryResponse: any; } export interface State { @@ -60,13 +59,19 @@ export class DataPanel extends Component { } hasPropsChanged(prevProps: Props) { - return this.props.refreshCounter !== prevProps.refreshCounter || this.props.isVisible !== prevProps.isVisible; + const { refreshCounter, isVisible, dataSourceApi } = this.props; + + return ( + refreshCounter !== prevProps.refreshCounter || + isVisible !== prevProps.isVisible || + dataSourceApi !== prevProps.dataSourceApi + ); } issueQueries = async () => { - const { isVisible, queries, datasource, panelId, dashboardId, timeRange } = this.props; + const { isVisible, queries, panelId, dashboardId, timeRange, dataSourceApi } = this.props; - if (!isVisible) { + if (!isVisible || !dataSourceApi) { return; } @@ -78,9 +83,6 @@ export class DataPanel extends Component { this.setState({ loading: LoadingState.Loading }); try { - const dataSourceSrv = getDatasourceSrv(); - const ds = await dataSourceSrv.get(datasource); - const queryOptions: DataQueryOptions = { timezone: 'browser', panelId: panelId, @@ -96,7 +98,7 @@ export class DataPanel extends Component { }; console.log('Issuing DataPanel query', queryOptions); - const resp = await ds.query(queryOptions); + const resp = await dataSourceApi.query(queryOptions); console.log('Issuing DataPanel query Resp', resp); this.setState({ @@ -104,6 +106,8 @@ export class DataPanel extends Component { response: resp, isFirstLoad: false, }); + + this.props.onIssueQueryResponse(resp.data); } catch (err) { console.log('Loading error', err); this.setState({ loading: LoadingState.Error, isFirstLoad: false }); diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 1a6f5a3cee2..4ac0723e4e8 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -3,43 +3,62 @@ import React, { ComponentClass, PureComponent } from 'react'; // Services import { getTimeSrv } from '../time_srv'; +import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; // Components import { PanelHeader } from './PanelHeader/PanelHeader'; import { DataPanel } from './DataPanel'; +import { PanelHeaderMenu } from './PanelHeader/PanelHeaderMenu'; // Types import { PanelModel } from '../panel_model'; import { DashboardModel } from '../dashboard_model'; -import { TimeRange, PanelProps } from 'app/types'; +import { TimeRange, PanelProps, TimeSeries } from 'app/types'; +import { DataSourceApi } from 'app/types/series'; export interface PanelChromeProps { panel: PanelModel; dashboard: DashboardModel; component: ComponentClass; - withMenuOptions: any; + withMenuOptions?: (c: typeof PanelHeaderMenu, p: PanelModel) => typeof PanelHeaderMenu; + moduleMenu?: any; } export interface PanelChromeState { refreshCounter: number; renderCounter: number; timeRange?: TimeRange; + timeSeries?: TimeSeries[]; + dataSourceApi?: DataSourceApi; } export class PanelChrome extends PureComponent { constructor(props) { super(props); - this.state = { refreshCounter: 0, renderCounter: 0, }; } - componentDidMount() { + async componentDidMount() { + const { panel } = this.props; + const { datasource } = panel; + this.props.panel.events.on('refresh', this.onRefresh); this.props.panel.events.on('render', this.onRender); this.props.dashboard.panelInitialized(this.props.panel); + + try { + const dataSourceSrv = getDatasourceSrv(); + const dataSourceApi = await dataSourceSrv.get(datasource); + this.setState(prevState => ({ + ...prevState, + dataSourceApi, + })); + } catch (err) { + console.log('Datasource loading error', err); + } } componentWillUnmount() { @@ -50,10 +69,11 @@ export class PanelChrome extends PureComponent ({ + ...prevState, refreshCounter: this.state.refreshCounter + 1, timeRange: timeRange, - }); + })); }; onRender = () => { @@ -63,27 +83,50 @@ export class PanelChrome extends PureComponent { + this.setState(prevState => ({ + ...prevState, + timeSeries, + })); + }; + get isVisible() { return !this.props.dashboard.otherPanelInFullscreen(this.props.panel); } render() { - const { panel, dashboard, withMenuOptions } = this.props; - const { datasource, targets } = panel; - const { timeRange, renderCounter, refreshCounter } = this.state; + const { panel, dashboard, moduleMenu } = this.props; + const { refreshCounter, timeRange, dataSourceApi, timeSeries, renderCounter } = this.state; + const { targets } = panel; const PanelComponent = this.props.component; console.log('Panel chrome render'); + // const PanelHeaderMenuComponent: typeof PanelHeaderMenu = withMenuOptions ? withMenuOptions(PanelHeaderMenu, panel) : PanelHeaderMenu; + const PanelHeaderMenuComponent = PanelHeaderMenu; + const mm = moduleMenu(panel, dataSourceApi, timeSeries); + const additionalMenuItems = mm.getAdditionalMenuItems || undefined; + const additionalSubMenuItems = mm.getAdditionalSubMenuItems || undefined; + console.log('panelChrome render'); return (
    - + + +
    {({ loading, timeSeries }) => { console.log('panelcrome inner render'); diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx index 3d23949afd0..ba5511014f2 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx @@ -1,21 +1,16 @@ import React, { PureComponent } from 'react'; import classNames from 'classnames'; -import { PanelModel } from 'app/features/dashboard/panel_model'; -import { DashboardModel } from 'app/features/dashboard/dashboard_model'; -import { PanelHeaderMenu } from './PanelHeaderMenu'; interface PanelHeaderProps { - panel: PanelModel; - dashboard: DashboardModel; - withMenuOptions: any; + title: string; } + export class PanelHeader extends PureComponent { render() { - const { dashboard, withMenuOptions, panel } = this.props; const isFullscreen = false; const isLoading = false; const panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen }); - const PanelHeaderMenuComponent = withMenuOptions ? withMenuOptions(PanelHeaderMenu, panel) : PanelHeaderMenu; + const { title } = this.props; return (
    @@ -34,10 +29,10 @@ export class PanelHeader extends PureComponent {
    - {this.props.panel.title} + {title} - + {this.props.children} 4m diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx index c36eb9d8584..dae9e33b996 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx @@ -1,28 +1,25 @@ import React, { PureComponent } from 'react'; import { DashboardModel } from 'app/features/dashboard/dashboard_model'; +import { PanelModel } from 'app/features/dashboard/panel_model'; import { PanelHeaderMenuItem, PanelHeaderMenuItemProps } from './PanelHeaderMenuItem'; import { getPanelMenu } from 'app/features/dashboard/utils/panel_menu'; +import { DataSourceApi } from 'app/types/series'; +import { TimeSeries } from 'app/types'; export interface PanelHeaderMenuProps { - panelId: number; + panel: PanelModel; dashboard: DashboardModel; - datasource: any; + dataSourceApi: DataSourceApi; additionalMenuItems?: PanelHeaderMenuItemProps[]; additionalSubMenuItems?: PanelHeaderMenuItemProps[]; + timeSeries?: TimeSeries[]; } export class PanelHeaderMenu extends PureComponent { - getPanel = () => { - // Pass in panel as prop instead? - const { panelId, dashboard } = this.props; - const panelInfo = dashboard.getPanelInfoById(panelId); - return panelInfo.panel; - }; - renderItems = (menu: PanelHeaderMenuItemProps[], isSubMenu = false) => { return (