Просмотр исходного кода

Merge remote-tracking branch 'upstream/master' into postgres-query-builder

Sven Klemm 7 лет назад
Родитель
Сommit
09efcbc205
75 измененных файлов с 520 добавлено и 220 удалено
  1. 20 2
      CHANGELOG.md
  2. 11 2
      docs/sources/administration/permissions.md
  3. 48 14
      docs/sources/guides/whats-new-in-v5.md
  4. 4 4
      docs/sources/http_api/alerting.md
  5. 12 9
      docs/sources/http_api/index.md
  6. 10 0
      docs/sources/installation/debian.md
  7. 7 0
      docs/sources/installation/rpm.md
  8. 5 0
      docs/sources/installation/upgrading.md
  9. 1 0
      docs/sources/installation/windows.md
  10. 1 1
      package.json
  11. 2 2
      packaging/publish/publish_testing.sh
  12. 1 1
      pkg/api/alerting.go
  13. 4 3
      pkg/api/dashboard.go
  14. 6 0
      pkg/api/dashboard_acl.go
  15. 1 1
      pkg/api/dtos/alerting.go
  16. 1 1
      pkg/api/dtos/dashboard.go
  17. 2 2
      pkg/api/login_oauth.go
  18. 1 2
      pkg/middleware/auth.go
  19. 3 0
      pkg/middleware/dashboard_redirect.go
  20. 4 2
      pkg/middleware/dashboard_redirect_test.go
  21. 3 1
      pkg/middleware/middleware.go
  22. 1 1
      pkg/middleware/recovery.go
  23. 5 0
      pkg/models/dashboard_acl.go
  24. 1 1
      pkg/models/dashboards.go
  25. 1 1
      pkg/services/alerting/eval_context.go
  26. 2 1
      pkg/services/search/models.go
  27. 7 2
      pkg/services/sqlstore/dashboard.go
  28. 26 6
      pkg/services/sqlstore/dashboard_acl.go
  29. 5 0
      pkg/services/sqlstore/dashboard_test.go
  30. 9 0
      pkg/services/sqlstore/migrations/dashboard_mig.go
  31. 1 0
      pkg/services/sqlstore/search_builder.go
  32. 1 1
      public/app/containers/AlertRuleList/AlertRuleList.jest.tsx
  33. 1 1
      public/app/containers/AlertRuleList/AlertRuleList.tsx
  34. 8 2
      public/app/containers/ManageDashboards/FolderPermissions.tsx
  35. 13 4
      public/app/containers/ManageDashboards/FolderSettings.jest.tsx
  36. 1 7
      public/app/core/angular_wrappers.ts
  37. 1 1
      public/app/core/components/Permissions/AddPermissions.jest.tsx
  38. 0 7
      public/app/core/components/Permissions/AddPermissions.tsx
  39. 9 6
      public/app/core/components/Permissions/DashboardPermissions.tsx
  40. 2 2
      public/app/core/components/Permissions/FolderInfo.ts
  41. 1 1
      public/app/core/components/Permissions/PermissionsListItem.tsx
  42. 3 2
      public/app/core/components/ScrollBar/ScrollBar.tsx
  43. 4 0
      public/app/core/components/grafana_app.ts
  44. 0 1
      public/app/core/components/help/help.ts
  45. 3 7
      public/app/core/components/org_switcher.ts
  46. 3 1
      public/app/core/components/scroll/scroll.ts
  47. 0 4
      public/app/core/directives/dash_class.js
  48. 2 2
      public/app/core/services/search_srv.ts
  49. 64 0
      public/app/core/specs/file_export.jest.ts
  50. 3 8
      public/app/core/specs/manage_dashboards.jest.ts
  51. 9 4
      public/app/core/specs/org_switcher.jest.ts
  52. 7 0
      public/app/core/specs/search_srv.jest.ts
  53. 53 6
      public/app/core/utils/file_export.ts
  54. 2 1
      public/app/features/dashboard/dashboard_srv.ts
  55. 0 1
      public/app/features/dashboard/dashgrid/AddPanelPanel.tsx
  56. 2 1
      public/app/features/dashboard/history/history.ts
  57. 5 4
      public/app/features/dashboard/settings/settings.html
  58. 15 1
      public/app/features/dashboard/settings/settings.ts
  59. 3 1
      public/app/features/panel/panel_directive.ts
  60. 1 1
      public/app/features/panel/solo_panel_ctrl.ts
  61. 1 1
      public/app/features/plugins/partials/ds_list.html
  62. 1 1
      public/app/plugins/panel/alertlist/module.html
  63. 1 0
      public/app/plugins/panel/graph/legend.ts
  64. 6 2
      public/app/plugins/panel/heatmap/heatmap_tooltip.ts
  65. 7 6
      public/app/routes/dashboard_loaders.ts
  66. 1 1
      public/app/stores/AlertListStore/AlertListStore.jest.ts
  67. 1 1
      public/app/stores/AlertListStore/AlertRule.ts
  68. 9 0
      public/app/stores/FolderStore/FolderStore.ts
  69. 7 3
      public/app/stores/PermissionsStore/PermissionsStore.ts
  70. 1 1
      public/sass/components/_dropdown.scss
  71. 1 0
      public/sass/components/_panel_dashlist.scss
  72. 6 1
      public/sass/components/_view_states.scss
  73. 0 28
      public/views/407.html
  74. 0 39
      public/views/500.html
  75. 57 0
      public/views/error.html

+ 20 - 2
CHANGELOG.md

@@ -1,6 +1,8 @@
-# 5.0.0 (unreleased / master branch)
+# 5.0.0-beta2 (unrelased)
 
 
-Grafana v5.0 is going to be the biggest and most foundational release Grafana has ever had, coming with a ton of UX improvements, a new dashboard grid engine, dashboard folders, user teams and permissions. Checkout out this [video preview](https://www.youtube.com/watch?v=BC_YRNpqj5k) of Grafana v5.
+# 5.0.0-beta1 (2018-02-05)
+
+Grafana v5.0 is going to be the biggest and most foundational release Grafana has ever had, coming with a ton of UX improvements, a new dashboard grid engine, dashboard folders, user teams and permissions. Checkout out this [video preview](https://www.youtube.com/watch?v=Izr0IBgoTZQ) of Grafana v5.
 
 
 ### New Major Features
 ### New Major Features
 - **Dashboards** Dashboard folders, [#1611](https://github.com/grafana/grafana/issues/1611)
 - **Dashboards** Dashboard folders, [#1611](https://github.com/grafana/grafana/issues/1611)
@@ -9,6 +11,7 @@ Grafana v5.0 is going to be the biggest and most foundational release Grafana ha
 - **Templating**: Vertical repeat direction for panel repeats.
 - **Templating**: Vertical repeat direction for panel repeats.
 - **UX**: Major update to page header and navigation
 - **UX**: Major update to page header and navigation
 - **Dashboard settings**: Combine dashboard settings views into one with side menu, [#9750](https://github.com/grafana/grafana/issues/9750)
 - **Dashboard settings**: Combine dashboard settings views into one with side menu, [#9750](https://github.com/grafana/grafana/issues/9750)
+- **Persistent dashboard url's**: New url's for dashboards that allows renaming dashboards without breaking links. [#7883](https://github.com/grafana/grafana/issues/7883)
 
 
 ## Breaking changes
 ## Breaking changes
 
 
@@ -18,6 +21,9 @@ From `/etc/grafana/datasources` to `/etc/grafana/provisioning/datasources` when
 
 
 * **Pagerduty** The notifier now defaults to not auto resolve incidents. More details at [#10222](https://github.com/grafana/grafana/issues/10222)
 * **Pagerduty** The notifier now defaults to not auto resolve incidents. More details at [#10222](https://github.com/grafana/grafana/issues/10222)
 
 
+* **HTTP API**
+  - `GET /api/alerts` property dashboardUri renamed to url and is now the full url (that is including app sub url).
+
 ## New Dashboard Grid
 ## New Dashboard Grid
 
 
 The new grid engine is a major upgrade for how you can position and move panels. It enables new layouts and a much easier dashboard building experience. The change is backward compatible. So you can upgrade your current version to 5.0 without breaking dashboards, but you cannot downgrade from 5.0 to previous versions. Grafana will automatically upgrade your dashboards to the new schema and position panels to match your existing layout. There might be minor differences in panel height. If you upgrade to 5.0 and for some reason want to rollback to the previous version you can restore dashboards to previous versions using dashboard history. But that should only be seen as an emergency solution.
 The new grid engine is a major upgrade for how you can position and move panels. It enables new layouts and a much easier dashboard building experience. The change is backward compatible. So you can upgrade your current version to 5.0 without breaking dashboards, but you cannot downgrade from 5.0 to previous versions. Grafana will automatically upgrade your dashboards to the new schema and position panels to match your existing layout. There might be minor differences in panel height. If you upgrade to 5.0 and for some reason want to rollback to the previous version you can restore dashboards to previous versions using dashboard history. But that should only be seen as an emergency solution.
@@ -58,10 +64,22 @@ Dashboard panels and rows are positioned using a gridPos object `{x: 0, y: 0, w:
 * **Singlestat**: suppress error when result contains no datapoints [#9636](https://github.com/grafana/grafana/issues/9636), thx [@utkarshcmu](https://github.com/utkarshcmu)
 * **Singlestat**: suppress error when result contains no datapoints [#9636](https://github.com/grafana/grafana/issues/9636), thx [@utkarshcmu](https://github.com/utkarshcmu)
 * **Postgres/MySQL**: Control quoting in SQL-queries when using template variables [#9030](https://github.com/grafana/grafana/issues/9030), thanks [@svenklemm](https://github.com/svenklemm)
 * **Postgres/MySQL**: Control quoting in SQL-queries when using template variables [#9030](https://github.com/grafana/grafana/issues/9030), thanks [@svenklemm](https://github.com/svenklemm)
 * **Pagerduty**: Pagerduty dont auto resolve incidents by default anymore. [#10222](https://github.com/grafana/grafana/issues/10222)
 * **Pagerduty**: Pagerduty dont auto resolve incidents by default anymore. [#10222](https://github.com/grafana/grafana/issues/10222)
+* **Cloudwatch**: Fix for multi-valued templated queries. [#9903](https://github.com/grafana/grafana/issues/9903)
 
 
 ## Tech
 ## Tech
 * **RabbitMq**: Remove support for publishing events to RabbitMQ [#9645](https://github.com/grafana/grafana/issues/9645)
 * **RabbitMq**: Remove support for publishing events to RabbitMQ [#9645](https://github.com/grafana/grafana/issues/9645)
 
 
+## Deprecation notes
+
+### HTTP API
+The following operations have been deprecated and will be removed in a future release:
+  - `GET /api/dashboards/db/:slug` -> Use `GET /api/dashboards/uid/:uid` instead
+  - `DELETE /api/dashboards/db/:slug` -> Use `DELETE /api/dashboards/uid/:uid` instead
+
+The following properties have been deprecated and will be removed in a future release:
+  - `uri` property in `GET /api/search` -> Use new `url` or `uid` property instead
+  - `meta.slug` property in `GET /api/dashboards/uid/:uid` and `GET /api/dashboards/db/:slug` -> Use new `meta.url` or `dashboard.uid` property instead
+
 # 4.6.3 (2017-12-14)
 # 4.6.3 (2017-12-14)
 
 
 ## Fixes
 ## Fixes

+ 11 - 2
docs/sources/administration/permissions.md

@@ -28,7 +28,7 @@ in that organization.
 
 
 Can do everything scoped to the organization. For example:
 Can do everything scoped to the organization. For example:
 
 
-- Add & Edit data data sources.
+- Add & Edit data sources.
 - Add & Edit organization users & teams.
 - Add & Edit organization users & teams.
 - Configure App plugins & set org settings.
 - Configure App plugins & set org settings.
 
 
@@ -73,4 +73,13 @@ The highest permission always wins so if you for example want to hide a folder o
 Access Control List (ACL).
 Access Control List (ACL).
 
 
 - You cannot override permissions for users with **Org Admin Role**
 - 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.
+- 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.
+

+ 48 - 14
docs/sources/guides/whats-new-in-v5.md

@@ -12,6 +12,8 @@ weight = -6
 
 
 # What's New in Grafana v5.0
 # What's New in Grafana v5.0
 
 
+> Out in beta: [Download now!](https://grafana.com/grafana/download/5.0.0-beta1)
+
 This is the most substantial update that Grafana has ever seen. This article will detail the major new features and enhancements.
 This is the most substantial update that Grafana has ever seen. This article will detail the major new features and enhancements.
 
 
 - [New Dashboard Layout Engine]({{< relref "#new-dashboard-layout-engine" >}}) enables a much easier drag, drop and resize experience and new types of layouts.
 - [New Dashboard Layout Engine]({{< relref "#new-dashboard-layout-engine" >}}) enables a much easier drag, drop and resize experience and new types of layouts.
@@ -22,10 +24,12 @@ This is the most substantial update that Grafana has ever seen. This article wil
 - [Group users into teams]({{< relref "#teams" >}}) and use them in the new permission system.
 - [Group users into teams]({{< relref "#teams" >}}) and use them in the new permission system.
 - [Datasource provisioning]({{< relref "#data-sources" >}}) makes it possible to setup datasources via config files.
 - [Datasource provisioning]({{< relref "#data-sources" >}}) makes it possible to setup datasources via config files.
 - [Dashboard provisioning]({{< relref "#dashboards" >}}) makes it possible to setup dashboards via config files.
 - [Dashboard provisioning]({{< relref "#dashboards" >}}) makes it possible to setup dashboards via config files.
+- [Persistent dashboard url's]({{< relref "#dashboard-model-persistent-url-s-and-api-changes" >}}) makes it possible to rename dashboards without breaking links.
+- [Graphite Tags & Integrated Function Docs]({{< relref "#graphite-tags-integrated-function-docs" >}}).
 
 
 ### Video showing new features
 ### Video showing new features
 
 
-<iframe height="215" src="https://www.youtube.com/embed/BC_YRNpqj5k?rel=0&amp;showinfo=0" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>
+<iframe width="450" height="270" src="https://www.youtube.com/embed/Izr0IBgoTZQ?rel=0&amp;" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>
 <br />
 <br />
 
 
 ## New Dashboard Layout Engine
 ## New Dashboard Layout Engine
@@ -36,7 +40,7 @@ The new dashboard layout engine allows for much easier movement and sizing of pa
 a very intuitive way. Panels are sized independently, so rows are no longer necessary to create layouts. This opens
 a very intuitive way. Panels are sized independently, so rows are no longer necessary to create layouts. This opens
 up many new types of layouts where panels of different heights can be aligned easily. Checkout the new grid in the video
 up many new types of layouts where panels of different heights can be aligned easily. Checkout the new grid in the video
 above or on the [play site](http://play.grafana.org). All your existing dashboards will automatically migrate to the
 above or on the [play site](http://play.grafana.org). All your existing dashboards will automatically migrate to the
-new position system and look close to identical. The new panel position makes dashboards saved in v5.0 not compatible
+new position system and look close to identical. The new panel position makes dashboards saved in v5.0 incompatible
 with older versions of Grafana.
 with older versions of Grafana.
 
 
 <div class="clearfix"></div>
 <div class="clearfix"></div>
@@ -49,7 +53,7 @@ Almost every page has seen significant UX improvements. All pages (except dashbo
 
 
 <div class="clearfix"></div>
 <div class="clearfix"></div>
 
 
-### Dashboard Settings
+## Dashboard Settings
 
 
 {{< docs-imagebox img="/img/docs/v50/dashboard_settings.png" max-width="1000px" class="docs-image--right" >}}
 {{< docs-imagebox img="/img/docs/v50/dashboard_settings.png" max-width="1000px" class="docs-image--right" >}}
 Dashboard pages have a new header toolbar where buttons and actions are now all moved to the right. All the dashboard
 Dashboard pages have a new header toolbar where buttons and actions are now all moved to the right. All the dashboard
@@ -61,7 +65,7 @@ settings views have been combined with a side nav which allows you to easily mov
 
 
 {{< docs-imagebox img="/img/docs/v50/new_white_theme.png" max-width="1000px" class="docs-image--right" >}}
 {{< docs-imagebox img="/img/docs/v50/new_white_theme.png" max-width="1000px" class="docs-image--right" >}}
 
 
-This theme has not seen a lot of love in recent years and we felt it was time to rework it and give it a major overhaul. We are very happy with the result.
+This theme has not seen a lot of love in recent years and we felt it was time to give it a major overhaul. We are very happy with the result.
 
 
 <div class="clearfix"></div>
 <div class="clearfix"></div>
 
 
@@ -78,22 +82,26 @@ which is very useful if you have a lot of dashboards or multiple teams.
 
 
 ## Teams
 ## Teams
 
 
-A team is a new concept in Grafana v5. They are simply a group of users that can be then be used in the new permission system for dashboards and folders. Only an admin can create teams.
+A team is a new concept in Grafana v5. They are simply a group of users that can be used in the new permission system for dashboards and folders. Only an admin can create teams.
 We hope to do more with teams in future releases like integration with LDAP and a team landing page.
 We hope to do more with teams in future releases like integration with LDAP and a team landing page.
 
 
 ## Permissions
 ## Permissions
 
 
 {{< docs-imagebox img="/img/docs/v50/folder_permissions.png" max-width="1000px" class="docs-image--right" >}}
 {{< docs-imagebox img="/img/docs/v50/folder_permissions.png" max-width="1000px" class="docs-image--right" >}}
 
 
-You can assign permissions to folders and dashboards. The default user role-based permissions can be removed and replaced with specific teams or users enabling more control over what a user can see and edit.
+You can assign permissions to folders and dashboards. The default user role-based permissions can be removed and
+replaced with specific teams or users enabling more control over what a user can see and edit.
+
+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.
 
 
 <div class="clearfix"></div>
 <div class="clearfix"></div>
 
 
-# Provisioning from configuration
+## Provisioning from configuration
 
 
 In previous versions of Grafana, you could only use the API for provisioning data sources and dashboards.
 In previous versions of Grafana, you could only use the API for provisioning data sources and dashboards.
 But that required the service to be running before you started creating dashboards and you also needed to
 But that required the service to be running before you started creating dashboards and you also needed to
-set up credentials for the HTTP API. In 5.0 we decided to improve this experience by adding a new active
+set up credentials for the HTTP API. In v5.0 we decided to improve this experience by adding a new active
 provisioning system that uses config files. This will make GitOps more natural as data sources and dashboards can
 provisioning system that uses config files. This will make GitOps more natural as data sources and dashboards can
 be defined via files that can be version controlled. We hope to extend this system to later add support for users, orgs
 be defined via files that can be version controlled. We hope to extend this system to later add support for users, orgs
 and alerts as well.
 and alerts as well.
@@ -111,10 +119,36 @@ in sync with dashboards in Grafana's database. The dashboard provisioner has mul
 which makes it possible to star them, use one as the home dashboard, set permissions and other features in Grafana that
 which makes it possible to star them, use one as the home dashboard, set permissions and other features in Grafana that
 expects the dashboards to exist in the database. More info in the [dashboard provisioning docs](/administration/provisioning/#dashboards)
 expects the dashboards to exist in the database. More info in the [dashboard provisioning docs](/administration/provisioning/#dashboards)
 
 
-# Dashboard model & API
 
 
-We are introducing a new identifier (`uid`) in the dashboard JSON model. The new identifier will be a 9-12 character long unique id.
-We are also changing the route for getting dashboards to use this `uid` instead of the slug that the current route and API are using.
-We will keep supporting the old route for backward compatibility. This will make it possible to change the title on dashboards without breaking links.
-Sharing dashboards between instances becomes much easier since the uid is unique (unique enough). This might seem like a small change,
-but we are incredibly excited about it since it will make it much easier to manage, collaborate and navigate between dashboards.
+## Graphite Tags & Integrated Function Docs
+
+{{< docs-imagebox img="/img/docs/v50/graphite_tags.png" max-width="1000px" class="docs-image--right" >}}
+
+The Graphite query editor has been updated to support the latest Graphite version (v1.2) that adds
+many new functions and support for querying by tags. You can now also view function documentation right in the query editor!
+
+Read more on [Graphite Tag Support](http://graphite.readthedocs.io/en/latest/tags.html?highlight=tags).
+
+<div class="clearfix"></div>
+
+## Dashboard model, persistent url's and API changes
+
+We are introducing a new unique identifier (`uid`) in the dashboard JSON model. It's automatically
+generated if not provided when creating a dashboard and will have a length of 9-12 characters.
+
+The unique identifier allows having persistent URL's for accessing dashboards, sharing them
+between instances and when using [dashboard provisioning](#dashboards). This means that dashboard can
+be renamed without breaking any links. We're changing the url format for dashboards
+from `/dashboard/db/:slug` to `/d/:uid/:slug`. We'll keep supporting the old slug-based url's for dashboards
+and redirects to the new one for backward compatibility. Please note that the old slug-based url's
+have been deprecated and will be removed in a future release.
+
+Sharing dashboards between instances becomes much easier since the `uid` is unique (unique enough).
+This might seem like a small change, but we are incredibly excited about it since it will make it
+much easier to manage, collaborate and navigate between dashboards.
+
+### API changes
+New uid-based routes in the dashboard API have been introduced to retrieve and delete dashboards.
+The corresponding slug-based routes have been deprecated and will be removed in a future release.
+
+

+ 4 - 4
docs/sources/http_api/alerting.md

@@ -62,7 +62,7 @@ Content-Type: application/json
       }
       }
     "newStateDate": "2016-12-25",
     "newStateDate": "2016-12-25",
     "executionError": "",
     "executionError": "",
-    "dashboardUri": "http://grafana.com/dashboard/db/sensors"
+    "url": "http://grafana.com/dashboard/db/sensors"
   }
   }
 ]
 ]
 ```
 ```
@@ -94,7 +94,7 @@ Content-Type: application/json
   "state": "alerting",
   "state": "alerting",
   "newStateDate": "2016-12-25",
   "newStateDate": "2016-12-25",
   "executionError": "",
   "executionError": "",
-  "dashboardUri": "http://grafana.com/dashboard/db/sensors"
+  "url": "http://grafana.com/dashboard/db/sensors"
 }
 }
 ```
 ```
 
 
@@ -196,7 +196,7 @@ Content-Type: application/json
 
 
 ## Create alert notification
 ## Create alert notification
 
 
-You can find the full list of [supported notifers](/alerting/notifications/#all-supported-notifier) at the alert notifiers page. 
+You can find the full list of [supported notifers](/alerting/notifications/#all-supported-notifier) at the alert notifiers page.
 
 
 `POST /api/alert-notifications`
 `POST /api/alert-notifications`
 
 
@@ -294,4 +294,4 @@ Content-Type: application/json
 {
 {
   "message": "Notification deleted"
   "message": "Notification deleted"
 }
 }
-```
+```

+ 12 - 9
docs/sources/http_api/index.md

@@ -18,12 +18,15 @@ dashboards, creating users and updating data sources.
 ## Supported HTTP APIs:
 ## Supported HTTP APIs:
 
 
 
 
-* [Authentication API]({{< relref "auth.md" >}})
-* [Dashboard API]({{< relref "dashboard.md" >}})
-* [Data Source API]({{< relref "data_source.md" >}})
-* [Organisation API]({{< relref "org.md" >}})
-* [User API]({{< relref "user.md" >}})
-* [Admin API]({{< relref "admin.md" >}})
-* [Snapshot API]({{< relref "snapshot.md" >}})
-* [Preferences API]({{< relref "preferences.md" >}})
-* [Other API]({{< relref "other.md" >}})
+* [Authentication API]({{< relref "/http_api/auth.md" >}})
+* [Dashboard API]({{< relref "/http_api/dashboard.md" >}})
+* [Dashboard Versions API]({{< relref "http_api/dashboard_versions.md" >}})
+* [Data Source API]({{< relref "http_api/data_source.md" >}})
+* [Organisation API]({{< relref "http_api/org.md" >}})
+* [Snapshot API]({{< relref "http_api/snapshot.md" >}})
+* [Annotations API]({{< relref "http_api/annotations.md" >}})
+* [Alerting API]({{< relref "http_api/alerting.md" >}})
+* [User API]({{< relref "http_api/user.md" >}})
+* [Admin API]({{< relref "http_api/admin.md" >}})
+* [Preferences API]({{< relref "http_api/preferences.md" >}})
+* [Other API]({{< relref "http_api/other.md" >}})

+ 10 - 0
docs/sources/installation/debian.md

@@ -16,6 +16,7 @@ weight = 1
 Description | Download
 Description | Download
 ------------ | -------------
 ------------ | -------------
 Stable for Debian-based Linux | [grafana_4.6.3_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.6.3_amd64.deb)
 Stable for Debian-based Linux | [grafana_4.6.3_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.6.3_amd64.deb)
+Beta for Debian-based Linux | [grafana_5.0.0-beta1_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.0.0-beta1_amd64.deb)
 
 
 Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
 Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
 installation.
 installation.
@@ -27,6 +28,15 @@ installation.
 wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.6.3_amd64.deb
 wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.6.3_amd64.deb
 sudo apt-get install -y adduser libfontconfig
 sudo apt-get install -y adduser libfontconfig
 sudo dpkg -i grafana_4.6.3_amd64.deb
 sudo dpkg -i grafana_4.6.3_amd64.deb
+```
+
+## Install Latest Beta
+
+```bash
+wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.0.0-beta1_amd64.deb
+sudo apt-get install -y adduser libfontconfig
+sudo dpkg -i grafana_5.0.0-beta1_amd64.deb
+
 ```
 ```
 ## APT Repository
 ## APT Repository
 
 

+ 7 - 0
docs/sources/installation/rpm.md

@@ -16,6 +16,7 @@ weight = 2
 Description | Download
 Description | Download
 ------------ | -------------
 ------------ | -------------
 Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [4.6.3 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.3-1.x86_64.rpm)
 Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [4.6.3 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.3-1.x86_64.rpm)
+Latest Beta for CentOS / Fedora / OpenSuse / Redhat Linux | [5.0.0-beta1 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.0-beta1.x86_64.rpm)
 
 
 Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
 Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
 installation.
 installation.
@@ -28,6 +29,12 @@ You can install Grafana using Yum directly.
 $ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.3-1.x86_64.rpm
 $ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.3-1.x86_64.rpm
 ```
 ```
 
 
+## Install Beta
+
+```bash
+$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.0-beta1.x86_64.rpm
+```
+
 Or install manually using `rpm`.
 Or install manually using `rpm`.
 
 
 #### On CentOS / Fedora / Redhat:
 #### On CentOS / Fedora / Redhat:

+ 5 - 0
docs/sources/installation/upgrading.md

@@ -101,3 +101,8 @@ as this will make upgrades easier without risking losing your config changes.
 ## Upgrading from 2.x
 ## Upgrading from 2.x
 
 
 We are not aware of any issues upgrading directly from 2.x to 4.x but to be on the safe side go via 3.x => 4.x.
 We are not aware of any issues upgrading directly from 2.x to 4.x but to be on the safe side go via 3.x => 4.x.
+
+## Upgrading to v5.0
+
+The dashboard grid layout engine has changed. All dashboards will be automatically upgraded to new
+positioning system when you load them in v5. Dashboards saved in v5 will not work in older versions of Grafana.

+ 1 - 0
docs/sources/installation/windows.md

@@ -14,6 +14,7 @@ weight = 3
 Description | Download
 Description | Download
 ------------ | -------------
 ------------ | -------------
 Latest stable package for Windows | [grafana.4.6.3.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.3.windows-x64.zip)
 Latest stable package for Windows | [grafana.4.6.3.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.3.windows-x64.zip)
+Latest beta package for Windows | [grafana.5.0.0-beta1.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.0-beta1.windows-x64.zip)
 
 
 Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
 Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
 installation.
 installation.

+ 1 - 1
package.json

@@ -4,7 +4,7 @@
     "company": "Grafana Labs"
     "company": "Grafana Labs"
   },
   },
   "name": "grafana",
   "name": "grafana",
-  "version": "5.0.0-pre1",
+  "version": "5.0.0-beta1",
   "repository": {
   "repository": {
     "type": "git",
     "type": "git",
     "url": "http://github.com/grafana/grafana.git"
     "url": "http://github.com/grafana/grafana.git"

+ 2 - 2
packaging/publish/publish_testing.sh

@@ -1,6 +1,6 @@
 #! /usr/bin/env bash
 #! /usr/bin/env bash
-deb_ver=4.6.0-beta1
-rpm_ver=4.6.0-beta1
+deb_ver=5.0.0-beta1
+rpm_ver=5.0.0-beta1
 
 
 wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_${deb_ver}_amd64.deb
 wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_${deb_ver}_amd64.deb
 
 

+ 1 - 1
pkg/api/alerting.go

@@ -105,7 +105,7 @@ func transformToDTOs(alerts []*models.Alert, c *middleware.Context) ([]*dtos.Ale
 	for _, alert := range alertDTOs {
 	for _, alert := range alertDTOs {
 		for _, dash := range dashboardsQuery.Result {
 		for _, dash := range dashboardsQuery.Result {
 			if alert.DashboardId == dash.Id {
 			if alert.DashboardId == dash.Id {
-				alert.DashbboardUri = dash.GenerateUrl()
+				alert.Url = dash.GenerateUrl()
 				break
 				break
 			}
 			}
 		}
 		}

+ 4 - 3
pkg/api/dashboard.go

@@ -99,7 +99,7 @@ func GetDashboard(c *middleware.Context) Response {
 			return ApiError(500, "Dashboard folder could not be read", err)
 			return ApiError(500, "Dashboard folder could not be read", err)
 		}
 		}
 		meta.FolderTitle = query.Result.Title
 		meta.FolderTitle = query.Result.Title
-		meta.FolderSlug = query.Result.Slug
+		meta.FolderUrl = query.Result.GetUrl()
 	}
 	}
 
 
 	// make sure db version is in sync with json model version
 	// make sure db version is in sync with json model version
@@ -293,10 +293,11 @@ func GetHomeDashboard(c *middleware.Context) Response {
 	}
 	}
 
 
 	if prefsQuery.Result.HomeDashboardId != 0 {
 	if prefsQuery.Result.HomeDashboardId != 0 {
-		slugQuery := m.GetDashboardSlugByIdQuery{Id: prefsQuery.Result.HomeDashboardId}
+		slugQuery := m.GetDashboardRefByIdQuery{Id: prefsQuery.Result.HomeDashboardId}
 		err := bus.Dispatch(&slugQuery)
 		err := bus.Dispatch(&slugQuery)
 		if err == nil {
 		if err == nil {
-			dashRedirect := dtos.DashboardRedirect{RedirectUri: "db/" + slugQuery.Result}
+			url := m.GetDashboardUrl(slugQuery.Result.Uid, slugQuery.Result.Slug)
+			dashRedirect := dtos.DashboardRedirect{RedirectUri: url}
 			return Json(200, &dashRedirect)
 			return Json(200, &dashRedirect)
 		} else {
 		} else {
 			log.Warn("Failed to get slug from database, %s", err.Error())
 			log.Warn("Failed to get slug from database, %s", err.Error())

+ 6 - 0
pkg/api/dashboard_acl.go

@@ -24,6 +24,12 @@ func GetDashboardAclList(c *middleware.Context) Response {
 		return ApiError(500, "Failed to get dashboard acl", err)
 		return ApiError(500, "Failed to get dashboard acl", err)
 	}
 	}
 
 
+	for _, perm := range acl {
+		if perm.Slug != "" {
+			perm.Url = m.GetDashboardFolderUrl(perm.IsFolder, perm.Uid, perm.Slug)
+		}
+	}
+
 	return Json(200, acl)
 	return Json(200, acl)
 }
 }
 
 

+ 1 - 1
pkg/api/dtos/alerting.go

@@ -19,7 +19,7 @@ type AlertRule struct {
 	EvalDate       time.Time        `json:"evalDate"`
 	EvalDate       time.Time        `json:"evalDate"`
 	EvalData       *simplejson.Json `json:"evalData"`
 	EvalData       *simplejson.Json `json:"evalData"`
 	ExecutionError string           `json:"executionError"`
 	ExecutionError string           `json:"executionError"`
-	DashbboardUri  string           `json:"dashboardUri"`
+	Url            string           `json:"url"`
 	CanEdit        bool             `json:"canEdit"`
 	CanEdit        bool             `json:"canEdit"`
 }
 }
 
 

+ 1 - 1
pkg/api/dtos/dashboard.go

@@ -27,7 +27,7 @@ type DashboardMeta struct {
 	IsFolder    bool      `json:"isFolder"`
 	IsFolder    bool      `json:"isFolder"`
 	FolderId    int64     `json:"folderId"`
 	FolderId    int64     `json:"folderId"`
 	FolderTitle string    `json:"folderTitle"`
 	FolderTitle string    `json:"folderTitle"`
-	FolderSlug  string    `json:"folderSlug"`
+	FolderUrl   string    `json:"folderUrl"`
 }
 }
 
 
 type DashboardFullWithMeta struct {
 type DashboardFullWithMeta struct {

+ 2 - 2
pkg/api/login_oauth.go

@@ -40,14 +40,14 @@ func GenStateString() string {
 
 
 func OAuthLogin(ctx *middleware.Context) {
 func OAuthLogin(ctx *middleware.Context) {
 	if setting.OAuthService == nil {
 	if setting.OAuthService == nil {
-		ctx.Handle(404, "login.OAuthLogin(oauth service not enabled)", nil)
+		ctx.Handle(404, "OAuth not enabled", nil)
 		return
 		return
 	}
 	}
 
 
 	name := ctx.Params(":name")
 	name := ctx.Params(":name")
 	connect, ok := social.SocialMap[name]
 	connect, ok := social.SocialMap[name]
 	if !ok {
 	if !ok {
-		ctx.Handle(404, "login.OAuthLogin(social login not enabled)", errors.New(name))
+		ctx.Handle(404, fmt.Sprintf("No OAuth with name %s configured", name), nil)
 		return
 		return
 	}
 	}
 
 

+ 1 - 2
pkg/middleware/auth.go

@@ -42,8 +42,7 @@ func accessForbidden(c *Context) {
 		return
 		return
 	}
 	}
 
 
-	c.SetCookie("redirect_to", url.QueryEscape(setting.AppSubUrl+c.Req.RequestURI), 0, setting.AppSubUrl+"/")
-	c.Redirect(setting.AppSubUrl + "/login")
+	c.Redirect(setting.AppSubUrl + "/")
 }
 }
 
 
 func notAuthorized(c *Context) {
 func notAuthorized(c *Context) {

+ 3 - 0
pkg/middleware/dashboard_redirect.go

@@ -1,6 +1,7 @@
 package middleware
 package middleware
 
 
 import (
 import (
+	"fmt"
 	"strings"
 	"strings"
 
 
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
@@ -24,6 +25,7 @@ func RedirectFromLegacyDashboardUrl() macaron.Handler {
 
 
 		if slug != "" {
 		if slug != "" {
 			if url, err := getDashboardUrlBySlug(c.OrgId, slug); err == nil {
 			if url, err := getDashboardUrlBySlug(c.OrgId, slug); err == nil {
+				url = fmt.Sprintf("%s?%s", url, c.Req.URL.RawQuery)
 				c.Redirect(url, 301)
 				c.Redirect(url, 301)
 				return
 				return
 			}
 			}
@@ -38,6 +40,7 @@ func RedirectFromLegacyDashboardSoloUrl() macaron.Handler {
 		if slug != "" {
 		if slug != "" {
 			if url, err := getDashboardUrlBySlug(c.OrgId, slug); err == nil {
 			if url, err := getDashboardUrlBySlug(c.OrgId, slug); err == nil {
 				url = strings.Replace(url, "/d/", "/d-solo/", 1)
 				url = strings.Replace(url, "/d/", "/d-solo/", 1)
+				url = fmt.Sprintf("%s?%s", url, c.Req.URL.RawQuery)
 				c.Redirect(url, 301)
 				c.Redirect(url, 301)
 				return
 				return
 			}
 			}

+ 4 - 2
pkg/middleware/dashboard_redirect_test.go

@@ -30,19 +30,20 @@ func TestMiddlewareDashboardRedirect(t *testing.T) {
 		middlewareScenario("GET dashboard by legacy url", func(sc *scenarioContext) {
 		middlewareScenario("GET dashboard by legacy url", func(sc *scenarioContext) {
 			sc.m.Get("/dashboard/db/:slug", redirectFromLegacyDashboardUrl, sc.defaultHandler)
 			sc.m.Get("/dashboard/db/:slug", redirectFromLegacyDashboardUrl, sc.defaultHandler)
 
 
-			sc.fakeReqWithParams("GET", "/dashboard/db/dash", map[string]string{}).exec()
+			sc.fakeReqWithParams("GET", "/dashboard/db/dash?orgId=1&panelId=2", map[string]string{}).exec()
 
 
 			Convey("Should redirect to new dashboard url with a 301 Moved Permanently", func() {
 			Convey("Should redirect to new dashboard url with a 301 Moved Permanently", func() {
 				So(sc.resp.Code, ShouldEqual, 301)
 				So(sc.resp.Code, ShouldEqual, 301)
 				redirectUrl, _ := sc.resp.Result().Location()
 				redirectUrl, _ := sc.resp.Result().Location()
 				So(redirectUrl.Path, ShouldEqual, m.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug))
 				So(redirectUrl.Path, ShouldEqual, m.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug))
+				So(len(redirectUrl.Query()), ShouldEqual, 2)
 			})
 			})
 		})
 		})
 
 
 		middlewareScenario("GET dashboard solo by legacy url", func(sc *scenarioContext) {
 		middlewareScenario("GET dashboard solo by legacy url", func(sc *scenarioContext) {
 			sc.m.Get("/dashboard-solo/db/:slug", redirectFromLegacyDashboardSoloUrl, sc.defaultHandler)
 			sc.m.Get("/dashboard-solo/db/:slug", redirectFromLegacyDashboardSoloUrl, sc.defaultHandler)
 
 
-			sc.fakeReqWithParams("GET", "/dashboard-solo/db/dash", map[string]string{}).exec()
+			sc.fakeReqWithParams("GET", "/dashboard-solo/db/dash?orgId=1&panelId=2", map[string]string{}).exec()
 
 
 			Convey("Should redirect to new dashboard url with a 301 Moved Permanently", func() {
 			Convey("Should redirect to new dashboard url with a 301 Moved Permanently", func() {
 				So(sc.resp.Code, ShouldEqual, 301)
 				So(sc.resp.Code, ShouldEqual, 301)
@@ -50,6 +51,7 @@ func TestMiddlewareDashboardRedirect(t *testing.T) {
 				expectedUrl := m.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug)
 				expectedUrl := m.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug)
 				expectedUrl = strings.Replace(expectedUrl, "/d/", "/d-solo/", 1)
 				expectedUrl = strings.Replace(expectedUrl, "/d/", "/d-solo/", 1)
 				So(redirectUrl.Path, ShouldEqual, expectedUrl)
 				So(redirectUrl.Path, ShouldEqual, expectedUrl)
+				So(len(redirectUrl.Query()), ShouldEqual, 2)
 			})
 			})
 		})
 		})
 	})
 	})

+ 3 - 1
pkg/middleware/middleware.go

@@ -206,7 +206,9 @@ func (ctx *Context) Handle(status int, title string, err error) {
 
 
 	ctx.Data["Title"] = title
 	ctx.Data["Title"] = title
 	ctx.Data["AppSubUrl"] = setting.AppSubUrl
 	ctx.Data["AppSubUrl"] = setting.AppSubUrl
-	ctx.HTML(status, strconv.Itoa(status))
+	ctx.Data["Theme"] = "dark"
+
+	ctx.HTML(status, "error")
 }
 }
 
 
 func (ctx *Context) JsonOK(message string) {
 func (ctx *Context) JsonOK(message string) {

+ 1 - 1
pkg/middleware/recovery.go

@@ -137,7 +137,7 @@ func Recovery() macaron.Handler {
 
 
 					c.JSON(500, resp)
 					c.JSON(500, resp)
 				} else {
 				} else {
-					c.HTML(500, "500")
+					c.HTML(500, "error")
 				}
 				}
 			}
 			}
 		}()
 		}()

+ 5 - 0
pkg/models/dashboard_acl.go

@@ -59,6 +59,11 @@ type DashboardAclInfoDTO struct {
 	Role           *RoleType      `json:"role,omitempty"`
 	Role           *RoleType      `json:"role,omitempty"`
 	Permission     PermissionType `json:"permission"`
 	Permission     PermissionType `json:"permission"`
 	PermissionName string         `json:"permissionName"`
 	PermissionName string         `json:"permissionName"`
+	Uid            string         `json:"uid"`
+	Title          string         `json:"title"`
+	Slug           string         `json:"slug"`
+	IsFolder       bool           `json:"isFolder"`
+	Url            string         `json:"url"`
 }
 }
 
 
 //
 //

+ 1 - 1
pkg/models/dashboards.go

@@ -293,7 +293,7 @@ type DashboardRef struct {
 	Slug string
 	Slug string
 }
 }
 
 
-type GetDashboardUIDByIdQuery struct {
+type GetDashboardRefByIdQuery struct {
 	Id     int64
 	Id     int64
 	Result *DashboardRef
 	Result *DashboardRef
 }
 }

+ 1 - 1
pkg/services/alerting/eval_context.go

@@ -90,7 +90,7 @@ func (c *EvalContext) GetDashboardUID() (*m.DashboardRef, error) {
 		return c.dashboardRef, nil
 		return c.dashboardRef, nil
 	}
 	}
 
 
-	uidQuery := &m.GetDashboardUIDByIdQuery{Id: c.Rule.DashboardId}
+	uidQuery := &m.GetDashboardRefByIdQuery{Id: c.Rule.DashboardId}
 	if err := bus.Dispatch(uidQuery); err != nil {
 	if err := bus.Dispatch(uidQuery); err != nil {
 		return nil, err
 		return nil, err
 	}
 	}

+ 2 - 1
pkg/services/search/models.go

@@ -21,8 +21,9 @@ type Hit struct {
 	Tags        []string `json:"tags"`
 	Tags        []string `json:"tags"`
 	IsStarred   bool     `json:"isStarred"`
 	IsStarred   bool     `json:"isStarred"`
 	FolderId    int64    `json:"folderId,omitempty"`
 	FolderId    int64    `json:"folderId,omitempty"`
+	FolderUid   string   `json:"folderUid,omitempty"`
 	FolderTitle string   `json:"folderTitle,omitempty"`
 	FolderTitle string   `json:"folderTitle,omitempty"`
-	FolderSlug  string   `json:"folderSlug,omitempty"`
+	FolderUrl   string   `json:"folderUrl,omitempty"`
 }
 }
 
 
 type HitList []*Hit
 type HitList []*Hit

+ 7 - 2
pkg/services/sqlstore/dashboard.go

@@ -245,6 +245,7 @@ type DashboardSearchProjection struct {
 	Term        string
 	Term        string
 	IsFolder    bool
 	IsFolder    bool
 	FolderId    int64
 	FolderId    int64
+	FolderUid   string
 	FolderSlug  string
 	FolderSlug  string
 	FolderTitle string
 	FolderTitle string
 }
 }
@@ -323,11 +324,15 @@ func makeQueryResult(query *search.FindPersistedDashboardsQuery, res []Dashboard
 				Url:         m.GetDashboardFolderUrl(item.IsFolder, item.Uid, item.Slug),
 				Url:         m.GetDashboardFolderUrl(item.IsFolder, item.Uid, item.Slug),
 				Type:        getHitType(item),
 				Type:        getHitType(item),
 				FolderId:    item.FolderId,
 				FolderId:    item.FolderId,
+				FolderUid:   item.FolderUid,
 				FolderTitle: item.FolderTitle,
 				FolderTitle: item.FolderTitle,
-				FolderSlug:  item.FolderSlug,
 				Tags:        []string{},
 				Tags:        []string{},
 			}
 			}
 
 
+			if item.FolderId > 0 {
+				hit.FolderUrl = m.GetFolderUrl(item.FolderUid, item.FolderSlug)
+			}
+
 			query.Result = append(query.Result, hit)
 			query.Result = append(query.Result, hit)
 			hits[item.Id] = hit
 			hits[item.Id] = hit
 		}
 		}
@@ -569,7 +574,7 @@ func GetDashboardsBySlug(query *m.GetDashboardsBySlugQuery) error {
 	return nil
 	return nil
 }
 }
 
 
-func GetDashboardUIDById(query *m.GetDashboardUIDByIdQuery) error {
+func GetDashboardUIDById(query *m.GetDashboardRefByIdQuery) error {
 	var rawSql = `SELECT uid, slug from dashboard WHERE Id=?`
 	var rawSql = `SELECT uid, slug from dashboard WHERE Id=?`
 
 
 	us := &m.DashboardRef{}
 	us := &m.DashboardRef{}

+ 26 - 6
pkg/services/sqlstore/dashboard_acl.go

@@ -113,6 +113,7 @@ func SetDashboardAcl(cmd *m.SetDashboardAclCommand) error {
 	})
 	})
 }
 }
 
 
+// RemoveDashboardAcl removes a specified permission from the dashboard acl
 func RemoveDashboardAcl(cmd *m.RemoveDashboardAclCommand) error {
 func RemoveDashboardAcl(cmd *m.RemoveDashboardAclCommand) error {
 	return inTransaction(func(sess *DBSession) error {
 	return inTransaction(func(sess *DBSession) error {
 		var rawSQL = "DELETE FROM " + dialect.Quote("dashboard_acl") + " WHERE org_id =? and id=?"
 		var rawSQL = "DELETE FROM " + dialect.Quote("dashboard_acl") + " WHERE org_id =? and id=?"
@@ -125,6 +126,11 @@ func RemoveDashboardAcl(cmd *m.RemoveDashboardAclCommand) error {
 	})
 	})
 }
 }
 
 
+// GetDashboardAclInfoList returns a list of permissions for a dashboard. They can be fetched from three
+// different places.
+// 1) Permissions for the dashboard
+// 2) permissions for its parent folder
+// 3) if no specific permissions have been set for the dashboard or its parent folder then get the default permissions
 func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error {
 func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error {
 	var err error
 	var err error
 
 
@@ -141,7 +147,11 @@ func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error {
 		da.updated,
 		da.updated,
 		'' as user_login,
 		'' as user_login,
 		'' as user_email,
 		'' as user_email,
-		'' as team
+		'' as team,
+		'' as title,
+		'' as slug,
+		'' as uid,` +
+			dialect.BooleanStr(false) + ` AS is_folder
 		FROM dashboard_acl as da
 		FROM dashboard_acl as da
 		WHERE da.dashboard_id = -1`
 		WHERE da.dashboard_id = -1`
 		query.Result = make([]*m.DashboardAclInfoDTO, 0)
 		query.Result = make([]*m.DashboardAclInfoDTO, 0)
@@ -155,6 +165,7 @@ func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error {
 		  )`, query.DashboardId, query.DashboardId)
 		  )`, query.DashboardId, query.DashboardId)
 
 
 		rawSQL := `
 		rawSQL := `
+			-- get permissions for the dashboard and its parent folder
 			SELECT
 			SELECT
 				da.id,
 				da.id,
 				da.org_id,
 				da.org_id,
@@ -167,13 +178,18 @@ func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error {
 				da.updated,
 				da.updated,
 				u.login AS user_login,
 				u.login AS user_login,
 				u.email AS user_email,
 				u.email AS user_email,
-				ug.name AS team
+				ug.name AS team,
+				d.title,
+				d.slug,
+				d.uid,
+				d.is_folder
 		  FROM` + dialect.Quote("dashboard_acl") + ` as da
 		  FROM` + dialect.Quote("dashboard_acl") + ` as da
 				LEFT OUTER JOIN ` + dialect.Quote("user") + ` AS u ON u.id = da.user_id
 				LEFT OUTER JOIN ` + dialect.Quote("user") + ` AS u ON u.id = da.user_id
 				LEFT OUTER JOIN team ug on ug.id = da.team_id
 				LEFT OUTER JOIN team ug on ug.id = da.team_id
+				LEFT OUTER JOIN dashboard d on da.dashboard_id = d.id
 			WHERE dashboard_id ` + dashboardFilter + ` AND da.org_id = ?
 			WHERE dashboard_id ` + dashboardFilter + ` AND da.org_id = ?
 
 
-			-- Also include default permission if has_acl = 0
+			-- Also include default permissions if folder or dashboard field "has_acl" is false
 
 
 			UNION
 			UNION
 				SELECT
 				SELECT
@@ -188,10 +204,14 @@ func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error {
 					da.updated,
 					da.updated,
 					'' as user_login,
 					'' as user_login,
 					'' as user_email,
 					'' as user_email,
-					'' as team
-					FROM dashboard_acl as da,
+					'' as team,
+					folder.title,
+					folder.slug,
+					folder.uid,
+					folder.is_folder
+				FROM dashboard_acl as da,
 				dashboard as dash
 				dashboard as dash
-				LEFT JOIN dashboard folder on dash.folder_id = folder.id
+				LEFT OUTER JOIN dashboard folder on dash.folder_id = folder.id
 					WHERE
 					WHERE
 						dash.id = ? AND (
 						dash.id = ? AND (
 							dash.has_acl = ` + dialect.BooleanStr(false) + ` or
 							dash.has_acl = ` + dialect.BooleanStr(false) + ` or

+ 5 - 0
pkg/services/sqlstore/dashboard_test.go

@@ -147,6 +147,7 @@ func TestDashboardDataAccess(t *testing.T) {
 				hit := query.Result[0]
 				hit := query.Result[0]
 				So(hit.Type, ShouldEqual, search.DashHitFolder)
 				So(hit.Type, ShouldEqual, search.DashHitFolder)
 				So(hit.Url, ShouldEqual, fmt.Sprintf("/dashboards/f/%s/%s", savedFolder.Uid, savedFolder.Slug))
 				So(hit.Url, ShouldEqual, fmt.Sprintf("/dashboards/f/%s/%s", savedFolder.Uid, savedFolder.Slug))
+				So(hit.FolderTitle, ShouldEqual, "")
 			})
 			})
 
 
 			Convey("Should be able to search for a dashboard folder's children", func() {
 			Convey("Should be able to search for a dashboard folder's children", func() {
@@ -163,6 +164,10 @@ func TestDashboardDataAccess(t *testing.T) {
 				hit := query.Result[0]
 				hit := query.Result[0]
 				So(hit.Id, ShouldEqual, savedDash.Id)
 				So(hit.Id, ShouldEqual, savedDash.Id)
 				So(hit.Url, ShouldEqual, fmt.Sprintf("/d/%s/%s", savedDash.Uid, savedDash.Slug))
 				So(hit.Url, ShouldEqual, fmt.Sprintf("/d/%s/%s", savedDash.Uid, savedDash.Slug))
+				So(hit.FolderId, ShouldEqual, savedFolder.Id)
+				So(hit.FolderUid, ShouldEqual, savedFolder.Uid)
+				So(hit.FolderTitle, ShouldEqual, savedFolder.Title)
+				So(hit.FolderUrl, ShouldEqual, fmt.Sprintf("/dashboards/f/%s/%s", savedFolder.Uid, savedFolder.Slug))
 			})
 			})
 
 
 			Convey("Should be able to search for dashboard by dashboard ids", func() {
 			Convey("Should be able to search for dashboard by dashboard ids", func() {

+ 9 - 0
pkg/services/sqlstore/migrations/dashboard_mig.go

@@ -167,4 +167,13 @@ func addDashboardMigration(mg *Migrator) {
 	mg.AddMigration("Remove unique index org_id_slug", NewDropIndexMigration(dashboardV2, &Index{
 	mg.AddMigration("Remove unique index org_id_slug", NewDropIndexMigration(dashboardV2, &Index{
 		Cols: []string{"org_id", "slug"}, Type: UniqueIndex,
 		Cols: []string{"org_id", "slug"}, Type: UniqueIndex,
 	}))
 	}))
+
+	mg.AddMigration("Update dashboard title length", NewTableCharsetMigration("dashboard", []*Column{
+		{Name: "title", Type: DB_NVarchar, Length: 189, Nullable: false},
+	}))
+
+	mg.AddMigration("Add unique index for dashboard_org_id_title_folder_id", NewAddIndexMigration(dashboardV2, &Index{
+		Cols: []string{"org_id", "folder_id", "title"}, Type: UniqueIndex,
+	}))
+
 }
 }

+ 1 - 0
pkg/services/sqlstore/search_builder.go

@@ -107,6 +107,7 @@ func (sb *SearchBuilder) buildSelect() {
 			dashboard_tag.term,
 			dashboard_tag.term,
 			dashboard.is_folder,
 			dashboard.is_folder,
 			dashboard.folder_id,
 			dashboard.folder_id,
+			folder.uid as folder_uid,
 			folder.slug as folder_slug,
 			folder.slug as folder_slug,
 			folder.title as folder_title
 			folder.title as folder_title
 		FROM `)
 		FROM `)

+ 1 - 1
public/app/containers/AlertRuleList/AlertRuleList.jest.tsx

@@ -23,7 +23,7 @@ describe('AlertRuleList', () => {
             .format(),
             .format(),
           evalData: {},
           evalData: {},
           executionError: '',
           executionError: '',
-          dashboardUri: 'd/ufkcofof/my-goal',
+          url: 'd/ufkcofof/my-goal',
           canEdit: true,
           canEdit: true,
         },
         },
       ])
       ])

+ 1 - 1
public/app/containers/AlertRuleList/AlertRuleList.tsx

@@ -137,7 +137,7 @@ export class AlertRuleItem extends React.Component<AlertRuleItemProps, any> {
       'fa-pause': !rule.isPaused,
       'fa-pause': !rule.isPaused,
     });
     });
 
 
-    let ruleUrl = `${rule.dashboardUri}?panelId=${rule.panelId}&fullscreen=true&edit=true&tab=alert`;
+    let ruleUrl = `${rule.url}?panelId=${rule.panelId}&fullscreen=true&edit=true&tab=alert`;
 
 
     return (
     return (
       <li className="alert-rule-item">
       <li className="alert-rule-item">

+ 8 - 2
public/app/containers/ManageDashboards/FolderPermissions.tsx

@@ -1,4 +1,4 @@
-import React, { Component } from 'react';
+import React, { Component } from 'react';
 import { inject, observer } from 'mobx-react';
 import { inject, observer } from 'mobx-react';
 import { toJS } from 'mobx';
 import { toJS } from 'mobx';
 import IContainerProps from 'app/containers/IContainerProps';
 import IContainerProps from 'app/containers/IContainerProps';
@@ -8,6 +8,7 @@ import Tooltip from 'app/core/components/Tooltip/Tooltip';
 import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo';
 import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo';
 import AddPermissions from 'app/core/components/Permissions/AddPermissions';
 import AddPermissions from 'app/core/components/Permissions/AddPermissions';
 import SlideDown from 'app/core/components/Animations/SlideDown';
 import SlideDown from 'app/core/components/Animations/SlideDown';
+
 @inject('nav', 'folder', 'view', 'permissions')
 @inject('nav', 'folder', 'view', 'permissions')
 @observer
 @observer
 export class FolderPermissions extends Component<IContainerProps, any> {
 export class FolderPermissions extends Component<IContainerProps, any> {
@@ -17,6 +18,11 @@ export class FolderPermissions extends Component<IContainerProps, any> {
     this.loadStore();
     this.loadStore();
   }
   }
 
 
+  componentWillUnmount() {
+    const { permissions } = this.props;
+    permissions.hideAddPermissions();
+  }
+
   loadStore() {
   loadStore() {
     const { nav, folder, view } = this.props;
     const { nav, folder, view } = this.props;
     return folder.load(view.routeParams.get('uid') as string).then(res => {
     return folder.load(view.routeParams.get('uid') as string).then(res => {
@@ -58,7 +64,7 @@ export class FolderPermissions extends Component<IContainerProps, any> {
             </button>
             </button>
           </div>
           </div>
           <SlideDown in={permissions.isAddPermissionsVisible}>
           <SlideDown in={permissions.isAddPermissionsVisible}>
-            <AddPermissions permissions={permissions} backendSrv={backendSrv} dashboardId={dashboardId} />
+            <AddPermissions permissions={permissions} backendSrv={backendSrv} />
           </SlideDown>
           </SlideDown>
           <Permissions permissions={permissions} isFolder={true} dashboardId={dashboardId} backendSrv={backendSrv} />
           <Permissions permissions={permissions} isFolder={true} dashboardId={dashboardId} backendSrv={backendSrv} />
         </div>
         </div>

+ 13 - 4
public/app/containers/ManageDashboards/FolderSettings.jest.tsx

@@ -14,6 +14,7 @@ describe('FolderSettings', () => {
         dashboard: {
         dashboard: {
           id: 1,
           id: 1,
           title: 'Folder Name',
           title: 'Folder Name',
+          uid: 'uid-str',
         },
         },
         meta: {
         meta: {
           url: '/dashboards/f/uid/folder-name',
           url: '/dashboards/f/uid/folder-name',
@@ -23,19 +24,27 @@ describe('FolderSettings', () => {
     );
     );
 
 
     const store = RootStore.create(
     const store = RootStore.create(
-      {},
+      {
+        view: {
+          path: 'asd',
+          query: {},
+          routeParams: {
+            uid: 'uid-str',
+          },
+        },
+      },
       {
       {
         backendSrv: backendSrv,
         backendSrv: backendSrv,
       }
       }
     );
     );
 
 
     wrapper = shallow(<FolderSettings backendSrv={backendSrv} {...store} />);
     wrapper = shallow(<FolderSettings backendSrv={backendSrv} {...store} />);
-    return wrapper
-      .dive()
+    page = wrapper.dive();
+    return page
       .instance()
       .instance()
       .loadStore()
       .loadStore()
       .then(() => {
       .then(() => {
-        page = wrapper.dive();
+        page.update();
       });
       });
   });
   });
 
 

+ 1 - 7
public/app/core/angular_wrappers.ts

@@ -20,11 +20,5 @@ export function registerAngularDirectives() {
     ['tagOptions', { watchDepth: 'reference' }],
     ['tagOptions', { watchDepth: 'reference' }],
   ]);
   ]);
   react2AngularDirective('selectUserPicker', UserPicker, ['backendSrv', 'handlePicked']);
   react2AngularDirective('selectUserPicker', UserPicker, ['backendSrv', 'handlePicked']);
-  react2AngularDirective('dashboardPermissions', DashboardPermissions, [
-    'backendSrv',
-    'dashboardId',
-    'folderTitle',
-    'folderSlug',
-    'folderId',
-  ]);
+  react2AngularDirective('dashboardPermissions', DashboardPermissions, ['backendSrv', 'dashboardId', 'folder']);
 }
 }

+ 1 - 1
public/app/core/components/Permissions/AddPermissions.jest.tsx

@@ -26,7 +26,7 @@ describe('AddPermissions', () => {
       }
       }
     );
     );
 
 
-    wrapper = shallow(<AddPermissions permissions={store.permissions} backendSrv={backendSrv} dashboardId={1} />);
+    wrapper = shallow(<AddPermissions permissions={store.permissions} backendSrv={backendSrv} />);
     instance = wrapper.instance();
     instance = wrapper.instance();
     return store.permissions.load(1, true, false);
     return store.permissions.load(1, true, false);
   });
   });

+ 0 - 7
public/app/core/components/Permissions/AddPermissions.tsx

@@ -9,7 +9,6 @@ import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore'
 export interface IProps {
 export interface IProps {
   permissions: any;
   permissions: any;
   backendSrv: any;
   backendSrv: any;
-  dashboardId: any;
 }
 }
 @observer
 @observer
 class AddPermissions extends Component<IProps, any> {
 class AddPermissions extends Component<IProps, any> {
@@ -31,12 +30,6 @@ class AddPermissions extends Component<IProps, any> {
     const { value } = evt.target;
     const { value } = evt.target;
     const { permissions } = this.props;
     const { permissions } = this.props;
 
 
-    // if (value === 'Viewer' || value === 'Editor') {
-    // //   permissions.addStoreItem({ permission: 1, role: value, dashboardId: dashboardId }, dashboardId);
-    // //   this.resetNewType();
-    //   return;
-    // }
-
     permissions.setNewType(value);
     permissions.setNewType(value);
   }
   }
 
 

+ 9 - 6
public/app/core/components/Permissions/DashboardPermissions.tsx

@@ -6,12 +6,11 @@ import Tooltip from 'app/core/components/Tooltip/Tooltip';
 import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo';
 import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo';
 import AddPermissions from 'app/core/components/Permissions/AddPermissions';
 import AddPermissions from 'app/core/components/Permissions/AddPermissions';
 import SlideDown from 'app/core/components/Animations/SlideDown';
 import SlideDown from 'app/core/components/Animations/SlideDown';
+import { FolderInfo } from './FolderInfo';
 
 
 export interface IProps {
 export interface IProps {
   dashboardId: number;
   dashboardId: number;
-  folderId: number;
-  folderTitle: string;
-  folderSlug: string;
+  folder?: FolderInfo;
   backendSrv: any;
   backendSrv: any;
 }
 }
 @observer
 @observer
@@ -28,8 +27,12 @@ class DashboardPermissions extends Component<IProps, any> {
     this.permissions.toggleAddPermissions();
     this.permissions.toggleAddPermissions();
   }
   }
 
 
+  componentWillUnmount() {
+    this.permissions.hideAddPermissions();
+  }
+
   render() {
   render() {
-    const { dashboardId, folderTitle, folderSlug, folderId, backendSrv } = this.props;
+    const { dashboardId, folder, backendSrv } = this.props;
 
 
     return (
     return (
       <div>
       <div>
@@ -50,13 +53,13 @@ class DashboardPermissions extends Component<IProps, any> {
           </div>
           </div>
         </div>
         </div>
         <SlideDown in={this.permissions.isAddPermissionsVisible}>
         <SlideDown in={this.permissions.isAddPermissionsVisible}>
-          <AddPermissions permissions={this.permissions} backendSrv={backendSrv} dashboardId={dashboardId} />
+          <AddPermissions permissions={this.permissions} backendSrv={backendSrv} />
         </SlideDown>
         </SlideDown>
         <Permissions
         <Permissions
           permissions={this.permissions}
           permissions={this.permissions}
           isFolder={false}
           isFolder={false}
           dashboardId={dashboardId}
           dashboardId={dashboardId}
-          folderInfo={{ title: folderTitle, slug: folderSlug, id: folderId }}
+          folderInfo={folder}
           backendSrv={backendSrv}
           backendSrv={backendSrv}
         />
         />
       </div>
       </div>

+ 2 - 2
public/app/core/components/Permissions/FolderInfo.ts

@@ -1,5 +1,5 @@
 export interface FolderInfo {
 export interface FolderInfo {
-  title: string;
   id: number;
   id: number;
-  slug: string;
+  title: string;
+  url: string;
 }
 }

+ 1 - 1
public/app/core/components/Permissions/PermissionsListItem.tsx

@@ -30,7 +30,7 @@ export default observer(({ item, removeItem, permissionChanged, itemIndex, folde
           folderInfo && (
           folderInfo && (
             <em className="muted no-wrap">
             <em className="muted no-wrap">
               Inherited from folder{' '}
               Inherited from folder{' '}
-              <a className="text-link" href={`dashboards/folder/${folderInfo.id}/${folderInfo.slug}/permissions`}>
+              <a className="text-link" href={`${folderInfo.url}/permissions`}>
                 {folderInfo.title}
                 {folderInfo.title}
               </a>{' '}
               </a>{' '}
             </em>
             </em>

+ 3 - 2
public/app/core/components/ScrollBar/ScrollBar.tsx

@@ -7,7 +7,6 @@ export interface Props {
 }
 }
 
 
 export default class ScrollBar extends React.Component<Props, any> {
 export default class ScrollBar extends React.Component<Props, any> {
-
   private container: any;
   private container: any;
   private ps: PerfectScrollbar;
   private ps: PerfectScrollbar;
 
 
@@ -16,7 +15,9 @@ export default class ScrollBar extends React.Component<Props, any> {
   }
   }
 
 
   componentDidMount() {
   componentDidMount() {
-    this.ps = new PerfectScrollbar(this.container);
+    this.ps = new PerfectScrollbar(this.container, {
+      wheelPropagation: true,
+    });
   }
   }
 
 
   componentDidUpdate() {
   componentDidUpdate() {

+ 4 - 0
public/app/core/components/grafana_app.ts

@@ -83,6 +83,10 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
         body.toggleClass('sidemenu-hidden');
         body.toggleClass('sidemenu-hidden');
       });
       });
 
 
+      scope.$watch(() => playlistSrv.isPlaying, function(newValue) {
+        elem.toggleClass('playlist-active', newValue === true);
+      });
+
       // tooltip removal fix
       // tooltip removal fix
       // manage page classes
       // manage page classes
       var pageClass;
       var pageClass;

+ 0 - 1
public/app/core/components/help/help.ts

@@ -19,7 +19,6 @@ export class HelpCtrl {
       ],
       ],
       Dashboard: [
       Dashboard: [
         { keys: ['mod+s'], description: 'Save dashboard' },
         { keys: ['mod+s'], description: 'Save dashboard' },
-        { keys: ['mod+h'], description: 'Hide row controls' },
         { keys: ['d', 'r'], description: 'Refresh all panels' },
         { keys: ['d', 'r'], description: 'Refresh all panels' },
         { keys: ['d', 's'], description: 'Dashboard settings' },
         { keys: ['d', 's'], description: 'Dashboard settings' },
         { keys: ['d', 'v'], description: 'Toggle in-active / view mode' },
         { keys: ['d', 'v'], description: 'Toggle in-active / view mode' },

+ 3 - 7
public/app/core/components/org_switcher.ts

@@ -1,5 +1,6 @@
 import coreModule from 'app/core/core_module';
 import coreModule from 'app/core/core_module';
 import { contextSrv } from 'app/core/services/context_srv';
 import { contextSrv } from 'app/core/services/context_srv';
+import config from 'app/core/config';
 
 
 const template = `
 const template = `
 <div class="modal-body">
 <div class="modal-body">
@@ -60,16 +61,11 @@ export class OrgSwitchCtrl {
 
 
   setUsingOrg(org) {
   setUsingOrg(org) {
     return this.backendSrv.post('/api/user/using/' + org.orgId).then(() => {
     return this.backendSrv.post('/api/user/using/' + org.orgId).then(() => {
-      const re = /orgId=\d+/gi;
-      this.setWindowLocationHref(this.getWindowLocationHref().replace(re, 'orgId=' + org.orgId));
+      this.setWindowLocation(config.appSubUrl + (config.appSubUrl.endsWith('/') ? '' : '/') + '?orgId=' + org.orgId);
     });
     });
   }
   }
 
 
-  getWindowLocationHref() {
-    return window.location.href;
-  }
-
-  setWindowLocationHref(href: string) {
+  setWindowLocation(href: string) {
     window.location.href = href;
     window.location.href = href;
   }
   }
 }
 }

+ 3 - 1
public/app/core/components/scroll/scroll.ts

@@ -6,7 +6,9 @@ export function geminiScrollbar() {
   return {
   return {
     restrict: 'A',
     restrict: 'A',
     link: function(scope, elem, attrs) {
     link: function(scope, elem, attrs) {
-      let scrollbar = new PerfectScrollbar(elem[0]);
+      let scrollbar = new PerfectScrollbar(elem[0], {
+        wheelPropagation: true,
+      });
       let lastPos = 0;
       let lastPos = 0;
 
 
       appEvents.on(
       appEvents.on(

+ 0 - 4
public/app/core/directives/dash_class.js

@@ -18,10 +18,6 @@ function (_, $, coreModule) {
           elem.toggleClass('panel-in-fullscreen', false);
           elem.toggleClass('panel-in-fullscreen', false);
         });
         });
 
 
-        $scope.$watch('ctrl.playlistSrv.isPlaying', function(newValue) {
-          elem.toggleClass('playlist-active', newValue === true);
-        });
-
         $scope.$watch('ctrl.dashboardViewState.state.editview', function(newValue) {
         $scope.$watch('ctrl.dashboardViewState.state.editview', function(newValue) {
           if (newValue) {
           if (newValue) {
             elem.toggleClass('dashboard-page--settings-opening', _.isString(newValue));
             elem.toggleClass('dashboard-page--settings-opening', _.isString(newValue));

+ 2 - 2
public/app/core/services/search_srv.ts

@@ -150,9 +150,9 @@ export class SearchSrv {
         if (hit.folderId) {
         if (hit.folderId) {
           section = {
           section = {
             id: hit.folderId,
             id: hit.folderId,
-            uid: hit.uid,
+            uid: hit.folderUid,
             title: hit.folderTitle,
             title: hit.folderTitle,
-            url: hit.url,
+            url: hit.folderUrl,
             items: [],
             items: [],
             icon: 'fa fa-folder-open',
             icon: 'fa fa-folder-open',
             toggle: this.toggleFolder.bind(this),
             toggle: this.toggleFolder.bind(this),

+ 64 - 0
public/app/core/specs/file_export.jest.ts

@@ -0,0 +1,64 @@
+import * as fileExport from '../utils/file_export';
+import { beforeEach, expect } from 'test/lib/common';
+
+describe('file_export', () => {
+  let ctx: any = {};
+
+  beforeEach(() => {
+    ctx.seriesList = [
+      {
+        alias: 'series_1',
+        datapoints: [
+          [1, 1500026100000],
+          [2, 1500026200000],
+          [null, 1500026300000],
+          [null, 1500026400000],
+          [null, 1500026500000],
+          [6, 1500026600000],
+        ],
+      },
+      {
+        alias: 'series_2',
+        datapoints: [[11, 1500026100000], [12, 1500026200000], [13, 1500026300000], [15, 1500026500000]],
+      },
+    ];
+
+    ctx.timeFormat = 'X'; // Unix timestamp (seconds)
+  });
+
+  describe('when exporting series as rows', () => {
+    it('should export points in proper order', () => {
+      let text = fileExport.convertSeriesListToCsv(ctx.seriesList, ctx.timeFormat);
+      const expectedText =
+        'Series;Time;Value\n' +
+        'series_1;1500026100;1\n' +
+        'series_1;1500026200;2\n' +
+        'series_1;1500026300;null\n' +
+        'series_1;1500026400;null\n' +
+        'series_1;1500026500;null\n' +
+        'series_1;1500026600;6\n' +
+        'series_2;1500026100;11\n' +
+        'series_2;1500026200;12\n' +
+        'series_2;1500026300;13\n' +
+        'series_2;1500026500;15\n';
+
+      expect(text).toBe(expectedText);
+    });
+  });
+
+  describe('when exporting series as columns', () => {
+    it('should export points in proper order', () => {
+      let text = fileExport.convertSeriesListToCsvColumns(ctx.seriesList, ctx.timeFormat);
+      const expectedText =
+        'Time;series_1;series_2\n' +
+        '1500026100;1;11\n' +
+        '1500026200;2;12\n' +
+        '1500026300;null;13\n' +
+        '1500026400;null;null\n' +
+        '1500026500;null;15\n' +
+        '1500026600;6;null\n';
+
+      expect(text).toBe(expectedText);
+    });
+  });
+});

+ 3 - 8
public/app/core/specs/manage_dashboards.jest.ts

@@ -20,9 +20,6 @@ describe('ManageDashboards', () => {
               icon: 'fa fa-folder',
               icon: 'fa fa-folder',
               tags: [],
               tags: [],
               isStarred: false,
               isStarred: false,
-              folderId: 410,
-              folderTitle: 'afolder',
-              folderSlug: 'afolder',
             },
             },
           ],
           ],
           tags: [],
           tags: [],
@@ -77,9 +74,6 @@ describe('ManageDashboards', () => {
               icon: 'fa fa-folder',
               icon: 'fa fa-folder',
               tags: [],
               tags: [],
               isStarred: false,
               isStarred: false,
-              folderId: 410,
-              folderTitle: 'afolder',
-              folderSlug: 'afolder',
             },
             },
           ],
           ],
           tags: [],
           tags: [],
@@ -112,8 +106,9 @@ describe('ManageDashboards', () => {
               tags: [],
               tags: [],
               isStarred: false,
               isStarred: false,
               folderId: 410,
               folderId: 410,
-              folderTitle: 'afolder',
-              folderSlug: 'afolder',
+              folderUid: 'uid',
+              folderTitle: 'Folder',
+              folderUrl: '/dashboards/f/uid/folder',
             },
             },
             {
             {
               id: 500,
               id: 500,

+ 9 - 4
public/app/core/specs/org_switcher.jest.ts

@@ -7,6 +7,12 @@ jest.mock('app/core/services/context_srv', () => ({
   },
   },
 }));
 }));
 
 
+jest.mock('app/core/config', () => {
+  return {
+    appSubUrl: '/subUrl',
+  };
+});
+
 describe('OrgSwitcher', () => {
 describe('OrgSwitcher', () => {
   describe('when switching org', () => {
   describe('when switching org', () => {
     let expectedHref;
     let expectedHref;
@@ -25,8 +31,7 @@ describe('OrgSwitcher', () => {
 
 
       const orgSwitcherCtrl = new OrgSwitchCtrl(backendSrvStub);
       const orgSwitcherCtrl = new OrgSwitchCtrl(backendSrvStub);
 
 
-      orgSwitcherCtrl.getWindowLocationHref = () => 'http://localhost:3000?orgId=1&from=now-3h&to=now';
-      orgSwitcherCtrl.setWindowLocationHref = href => (expectedHref = href);
+      orgSwitcherCtrl.setWindowLocation = href => (expectedHref = href);
 
 
       return orgSwitcherCtrl.setUsingOrg({ orgId: 2 });
       return orgSwitcherCtrl.setUsingOrg({ orgId: 2 });
     });
     });
@@ -35,8 +40,8 @@ describe('OrgSwitcher', () => {
       expect(expectedUsingUrl).toBe('/api/user/using/2');
       expect(expectedUsingUrl).toBe('/api/user/using/2');
     });
     });
 
 
-    it('should switch orgId in url', () => {
-      expect(expectedHref).toBe('http://localhost:3000?orgId=2&from=now-3h&to=now');
+    it('should switch orgId in url and redirect to home page', () => {
+      expect(expectedHref).toBe('/subUrl/?orgId=2');
     });
     });
   });
   });
 });
 });

+ 7 - 0
public/app/core/specs/search_srv.jest.ts

@@ -190,7 +190,9 @@ describe('SearchSrv', () => {
             title: 'dash in folder1 1',
             title: 'dash in folder1 1',
             type: 'dash-db',
             type: 'dash-db',
             folderId: 1,
             folderId: 1,
+            folderUid: 'uid',
             folderTitle: 'folder1',
             folderTitle: 'folder1',
+            folderUrl: '/dashboards/f/uid/folder1',
           },
           },
         ])
         ])
       );
       );
@@ -206,6 +208,11 @@ describe('SearchSrv', () => {
 
 
     it('should group results by folder', () => {
     it('should group results by folder', () => {
       expect(results).toHaveLength(2);
       expect(results).toHaveLength(2);
+      expect(results[0].id).toEqual(0);
+      expect(results[1].id).toEqual(1);
+      expect(results[1].uid).toEqual('uid');
+      expect(results[1].title).toEqual('folder1');
+      expect(results[1].url).toEqual('/dashboards/f/uid/folder1');
     });
     });
   });
   });
 
 

+ 53 - 6
public/app/core/utils/file_export.ts

@@ -3,19 +3,27 @@ import moment from 'moment';
 import { saveAs } from 'file-saver';
 import { saveAs } from 'file-saver';
 
 
 const DEFAULT_DATETIME_FORMAT = 'YYYY-MM-DDTHH:mm:ssZ';
 const DEFAULT_DATETIME_FORMAT = 'YYYY-MM-DDTHH:mm:ssZ';
+const POINT_TIME_INDEX = 1;
+const POINT_VALUE_INDEX = 0;
 
 
-export function exportSeriesListToCsv(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) {
+export function convertSeriesListToCsv(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) {
   var text = (excel ? 'sep=;\n' : '') + 'Series;Time;Value\n';
   var text = (excel ? 'sep=;\n' : '') + 'Series;Time;Value\n';
   _.each(seriesList, function(series) {
   _.each(seriesList, function(series) {
     _.each(series.datapoints, function(dp) {
     _.each(series.datapoints, function(dp) {
-      text += series.alias + ';' + moment(dp[1]).format(dateTimeFormat) + ';' + dp[0] + '\n';
+      text +=
+        series.alias + ';' + moment(dp[POINT_TIME_INDEX]).format(dateTimeFormat) + ';' + dp[POINT_VALUE_INDEX] + '\n';
     });
     });
   });
   });
+  return text;
+}
+
+export function exportSeriesListToCsv(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) {
+  var text = convertSeriesListToCsv(seriesList, dateTimeFormat, excel);
   saveSaveBlob(text, 'grafana_data_export.csv');
   saveSaveBlob(text, 'grafana_data_export.csv');
 }
 }
 
 
-export function exportSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) {
-  var text = (excel ? 'sep=;\n' : '') + 'Time;';
+export function convertSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) {
+  let text = (excel ? 'sep=;\n' : '') + 'Time;';
   // add header
   // add header
   _.each(seriesList, function(series) {
   _.each(seriesList, function(series) {
     text += series.alias + ';';
     text += series.alias + ';';
@@ -24,14 +32,15 @@ export function exportSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAUL
   text += '\n';
   text += '\n';
 
 
   // process data
   // process data
+  seriesList = mergeSeriesByTime(seriesList);
   var dataArr = [[]];
   var dataArr = [[]];
   var sIndex = 1;
   var sIndex = 1;
   _.each(seriesList, function(series) {
   _.each(seriesList, function(series) {
     var cIndex = 0;
     var cIndex = 0;
     dataArr.push([]);
     dataArr.push([]);
     _.each(series.datapoints, function(dp) {
     _.each(series.datapoints, function(dp) {
-      dataArr[0][cIndex] = moment(dp[1]).format(dateTimeFormat);
-      dataArr[sIndex][cIndex] = dp[0];
+      dataArr[0][cIndex] = moment(dp[POINT_TIME_INDEX]).format(dateTimeFormat);
+      dataArr[sIndex][cIndex] = dp[POINT_VALUE_INDEX];
       cIndex++;
       cIndex++;
     });
     });
     sIndex++;
     sIndex++;
@@ -46,6 +55,44 @@ export function exportSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAUL
     text = text.substring(0, text.length - 1);
     text = text.substring(0, text.length - 1);
     text += '\n';
     text += '\n';
   }
   }
+
+  return text;
+}
+
+/**
+ * Collect all unique timestamps from series list and use it to fill
+ * missing points by null.
+ */
+function mergeSeriesByTime(seriesList) {
+  let timestamps = [];
+  for (let i = 0; i < seriesList.length; i++) {
+    let seriesPoints = seriesList[i].datapoints;
+    for (let j = 0; j < seriesPoints.length; j++) {
+      timestamps.push(seriesPoints[j][POINT_TIME_INDEX]);
+    }
+  }
+  timestamps = _.sortedUniq(timestamps.sort());
+
+  for (let i = 0; i < seriesList.length; i++) {
+    let seriesPoints = seriesList[i].datapoints;
+    let seriesTimestamps = _.map(seriesPoints, p => p[POINT_TIME_INDEX]);
+    let extendedSeries = [];
+    let pointIndex;
+    for (let j = 0; j < timestamps.length; j++) {
+      pointIndex = _.sortedIndexOf(seriesTimestamps, timestamps[j]);
+      if (pointIndex !== -1) {
+        extendedSeries.push(seriesPoints[pointIndex]);
+      } else {
+        extendedSeries.push([null, timestamps[j]]);
+      }
+    }
+    seriesList[i].datapoints = extendedSeries;
+  }
+  return seriesList;
+}
+
+export function exportSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) {
+  let text = convertSeriesListToCsvColumns(seriesList, dateTimeFormat, excel);
   saveSaveBlob(text, 'grafana_data_export.csv');
   saveSaveBlob(text, 'grafana_data_export.csv');
 }
 }
 
 

+ 2 - 1
public/app/features/dashboard/dashboard_srv.ts

@@ -1,5 +1,6 @@
 import coreModule from 'app/core/core_module';
 import coreModule from 'app/core/core_module';
 import { DashboardModel } from './dashboard_model';
 import { DashboardModel } from './dashboard_model';
+import locationUtil from 'app/core/utils/location_util';
 
 
 export class DashboardSrv {
 export class DashboardSrv {
   dash: any;
   dash: any;
@@ -74,7 +75,7 @@ export class DashboardSrv {
     this.dash.version = data.version;
     this.dash.version = data.version;
 
 
     if (data.url !== this.$location.path()) {
     if (data.url !== this.$location.path()) {
-      this.$location.url(data.url);
+      this.$location.url(locationUtil.stripBaseFromUrl(data.url)).replace();
     }
     }
 
 
     this.$rootScope.appEvent('dashboard-saved', this.dash);
     this.$rootScope.appEvent('dashboard-saved', this.dash);

+ 0 - 1
public/app/features/dashboard/dashgrid/AddPanelPanel.tsx

@@ -93,7 +93,6 @@ export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelP
   }
   }
 
 
   renderPanelItem(panel, index) {
   renderPanelItem(panel, index) {
-    console.log('render panel', index);
     return (
     return (
       <div key={index} className="add-panel__item" onClick={() => this.onAddPanel(panel)} title={panel.name}>
       <div key={index} className="add-panel__item" onClick={() => this.onAddPanel(panel)} title={panel.name}>
         <img className="add-panel__item-img" src={panel.info.logos.small} />
         <img className="add-panel__item-img" src={panel.info.logos.small} />

+ 2 - 1
public/app/features/dashboard/history/history.ts

@@ -4,6 +4,7 @@ import _ from 'lodash';
 import angular from 'angular';
 import angular from 'angular';
 import moment from 'moment';
 import moment from 'moment';
 
 
+import locationUtil from 'app/core/utils/location_util';
 import { DashboardModel } from '../dashboard_model';
 import { DashboardModel } from '../dashboard_model';
 import { HistoryListOpts, RevisionsModel, CalculateDiffOptions, HistorySrv } from './history_srv';
 import { HistoryListOpts, RevisionsModel, CalculateDiffOptions, HistorySrv } from './history_srv';
 
 
@@ -185,7 +186,7 @@ export class HistoryListCtrl {
     return this.historySrv
     return this.historySrv
       .restoreDashboard(this.dashboard, version)
       .restoreDashboard(this.dashboard, version)
       .then(response => {
       .then(response => {
-        this.$location.path('dashboard/db/' + response.slug);
+        this.$location.url(locationUtil.stripBaseFromUrl(response.url)).replace();
         this.$route.reload();
         this.$route.reload();
         this.$rootScope.appEvent('alert-success', ['Dashboard restored', 'Restored from version ' + version]);
         this.$rootScope.appEvent('alert-success', ['Dashboard restored', 'Restored from version ' + version]);
       })
       })

+ 5 - 4
public/app/features/dashboard/settings/settings.html

@@ -96,13 +96,14 @@
 </div>
 </div>
 
 
 <div class="dashboard-settings__content" ng-if="ctrl.viewId === 'permissions'" >
 <div class="dashboard-settings__content" ng-if="ctrl.viewId === 'permissions'" >
-  <dashboard-permissions ng-if="ctrl.dashboard"
+  <dashboard-permissions ng-if="ctrl.dashboard && !ctrl.hasUnsavedFolderChange"
     dashboardId="ctrl.dashboard.id"
     dashboardId="ctrl.dashboard.id"
     backendSrv="ctrl.backendSrv"
     backendSrv="ctrl.backendSrv"
-    folderTitle="ctrl.dashboard.meta.folderTitle"
-    folderSlug="ctrl.dashboard.meta.folderSlug"
-    folderId="ctrl.dashboard.meta.folderId"
+    folder="ctrl.getFolder()"
   />
   />
+  <div ng-if="ctrl.hasUnsavedFolderChange">
+    <h5>You have changed folder, please save to view permissions.</h5>
+  </div>
 </div>
 </div>
 
 
 <div class="dashboard-settings__content" ng-if="ctrl.viewId === '404'">
 <div class="dashboard-settings__content" ng-if="ctrl.viewId === '404'">

+ 15 - 1
public/app/features/dashboard/settings/settings.ts

@@ -14,6 +14,7 @@ export class SettingsCtrl {
   canSave: boolean;
   canSave: boolean;
   canDelete: boolean;
   canDelete: boolean;
   sections: any[];
   sections: any[];
+  hasUnsavedFolderChange: boolean;
 
 
   /** @ngInject */
   /** @ngInject */
   constructor(private $scope, private $location, private $rootScope, private backendSrv, private dashboardSrv) {
   constructor(private $scope, private $location, private $rootScope, private backendSrv, private dashboardSrv) {
@@ -38,6 +39,7 @@ export class SettingsCtrl {
 
 
     this.$rootScope.onAppEvent('$routeUpdate', this.onRouteUpdated.bind(this), $scope);
     this.$rootScope.onAppEvent('$routeUpdate', this.onRouteUpdated.bind(this), $scope);
     this.$rootScope.appEvent('dash-scroll', { animate: false, pos: 0 });
     this.$rootScope.appEvent('dash-scroll', { animate: false, pos: 0 });
+    this.$rootScope.onAppEvent('dashboard-saved', this.onPostSave.bind(this), $scope);
   }
   }
 
 
   buildSectionList() {
   buildSectionList() {
@@ -135,6 +137,10 @@ export class SettingsCtrl {
     this.dashboardSrv.saveDashboard();
     this.dashboardSrv.saveDashboard();
   }
   }
 
 
+  onPostSave() {
+    this.hasUnsavedFolderChange = false;
+  }
+
   hideSettings() {
   hideSettings() {
     var urlParams = this.$location.search();
     var urlParams = this.$location.search();
     delete urlParams.editview;
     delete urlParams.editview;
@@ -195,7 +201,15 @@ export class SettingsCtrl {
   onFolderChange(folder) {
   onFolderChange(folder) {
     this.dashboard.meta.folderId = folder.id;
     this.dashboard.meta.folderId = folder.id;
     this.dashboard.meta.folderTitle = folder.title;
     this.dashboard.meta.folderTitle = folder.title;
-    this.dashboard.meta.folderSlug = folder.slug;
+    this.hasUnsavedFolderChange = true;
+  }
+
+  getFolder() {
+    return {
+      id: this.dashboard.meta.folderId,
+      title: this.dashboard.meta.folderTitle,
+      url: this.dashboard.meta.folderUrl,
+    };
   }
   }
 }
 }
 
 

+ 3 - 1
public/app/features/panel/panel_directive.ts

@@ -100,7 +100,9 @@ module.directive('grafanaPanel', function($rootScope, $document, $timeout) {
       // update scrollbar after mounting
       // update scrollbar after mounting
       ctrl.events.on('component-did-mount', () => {
       ctrl.events.on('component-did-mount', () => {
         if (ctrl.__proto__.constructor.scrollable) {
         if (ctrl.__proto__.constructor.scrollable) {
-          panelScrollbar = new PerfectScrollbar(panelContent[0]);
+          panelScrollbar = new PerfectScrollbar(panelContent[0], {
+            wheelPropagation: true,
+          });
         }
         }
       });
       });
 
 

+ 1 - 1
public/app/features/panel/solo_panel_ctrl.ts

@@ -9,7 +9,7 @@ export class SoloPanelCtrl {
 
 
     $scope.init = function() {
     $scope.init = function() {
       contextSrv.sidemenu = false;
       contextSrv.sidemenu = false;
-      appEvents.emit('toggle-sidemenu');
+      appEvents.emit('toggle-sidemenu-hidden');
 
 
       var params = $location.search();
       var params = $location.search();
       panelId = parseInt(params.panelId);
       panelId = parseInt(params.panelId);

+ 1 - 1
public/app/features/plugins/partials/ds_list.html

@@ -52,7 +52,7 @@
 		<empty-list-cta model="{
 		<empty-list-cta model="{
 			title: 'There are no data sources defined yet',
 			title: 'There are no data sources defined yet',
 			buttonIcon: 'gicon gicon-add-datasources',
 			buttonIcon: 'gicon gicon-add-datasources',
-			buttonLink: '/datasources/new',
+			buttonLink: 'datasources/new',
 			buttonTitle: 'Add data source',
 			buttonTitle: 'Add data source',
 			proTip: 'You can also define data sources through configuration files.',
 			proTip: 'You can also define data sources through configuration files.',
 			proTipLink: 'http://docs.grafana.org/administration/provisioning/#datasources?utm_source=grafana_ds_list',
 			proTipLink: 'http://docs.grafana.org/administration/provisioning/#datasources?utm_source=grafana_ds_list',

+ 1 - 1
public/app/plugins/panel/alertlist/module.html

@@ -12,7 +12,7 @@
         <div class="alert-rule-item__body">
         <div class="alert-rule-item__body">
           <div class="alert-rule-item__header">
           <div class="alert-rule-item__header">
             <p class="alert-rule-item__name">
             <p class="alert-rule-item__name">
-              <a href="dashboard/{{alert.dashboardUri}}?panelId={{alert.panelId}}&fullscreen&edit&tab=alert">
+              <a href="{{alert.url}}?panelId={{alert.panelId}}&fullscreen&edit&tab=alert">
                 {{alert.name}}
                 {{alert.name}}
               </a>
               </a>
             </p>
             </p>

+ 1 - 0
public/app/plugins/panel/graph/legend.ts

@@ -246,6 +246,7 @@ module.directive('graphLegend', function(popoverSrv, $timeout) {
           // Number of pixels the content height can surpass the container height without enabling the scroll bar.
           // Number of pixels the content height can surpass the container height without enabling the scroll bar.
           scrollYMarginOffset: 2,
           scrollYMarginOffset: 2,
           suppressScrollX: true,
           suppressScrollX: true,
+          wheelPropagation: true,
         };
         };
 
 
         if (!legendScrollbar) {
         if (!legendScrollbar) {

+ 6 - 2
public/app/plugins/panel/heatmap/heatmap_tooltip.ts

@@ -153,8 +153,12 @@ export class HeatmapTooltip {
 
 
   getXBucketIndex(offsetX, data) {
   getXBucketIndex(offsetX, data) {
     let x = this.scope.xScale.invert(offsetX - this.scope.yAxisWidth).valueOf();
     let x = this.scope.xScale.invert(offsetX - this.scope.yAxisWidth).valueOf();
-    let xBucketIndex = getValueBucketBound(x, data.xBucketSize, 1);
-    return xBucketIndex;
+    // First try to find X bucket by checking x pos is in the
+    // [bucket.x, bucket.x + xBucketSize] interval
+    let xBucket = _.find(data.buckets, bucket => {
+      return x > bucket.x && x - bucket.x <= data.xBucketSize;
+    });
+    return xBucket ? xBucket.x : getValueBucketBound(x, data.xBucketSize, 1);
   }
   }
 
 
   getYBucketIndex(offsetY, data) {
   getYBucketIndex(offsetY, data) {

+ 7 - 6
public/app/routes/dashboard_loaders.ts

@@ -9,7 +9,7 @@ export class LoadDashboardCtrl {
     if (!$routeParams.uid && !$routeParams.slug) {
     if (!$routeParams.uid && !$routeParams.slug) {
       backendSrv.get('/api/dashboards/home').then(function(homeDash) {
       backendSrv.get('/api/dashboards/home').then(function(homeDash) {
         if (homeDash.redirectUri) {
         if (homeDash.redirectUri) {
-          $location.path('dashboard/' + homeDash.redirectUri);
+          $location.path(homeDash.redirectUri);
         } else {
         } else {
           var meta = homeDash.meta;
           var meta = homeDash.meta;
           meta.canSave = meta.canShare = meta.canStar = false;
           meta.canSave = meta.canShare = meta.canStar = false;
@@ -23,18 +23,19 @@ export class LoadDashboardCtrl {
     if (!($routeParams.type === 'script' || $routeParams.type === 'snapshot') && !$routeParams.uid) {
     if (!($routeParams.type === 'script' || $routeParams.type === 'snapshot') && !$routeParams.uid) {
       backendSrv.get(`/api/dashboards/db/${$routeParams.slug}`).then(res => {
       backendSrv.get(`/api/dashboards/db/${$routeParams.slug}`).then(res => {
         if (res) {
         if (res) {
-          const url = locationUtil.stripBaseFromUrl(res.meta.url);
-          $location.path(url).replace();
+          $location.path(locationUtil.stripBaseFromUrl(res.meta.url)).replace();
         }
         }
       });
       });
       return;
       return;
     }
     }
 
 
     dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug, $routeParams.uid).then(function(result) {
     dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug, $routeParams.uid).then(function(result) {
-      const url = locationUtil.stripBaseFromUrl(result.meta.url);
+      if (result.meta.url) {
+        const url = locationUtil.stripBaseFromUrl(result.meta.url);
 
 
-      if (url !== $location.path()) {
-        $location.path(url).replace();
+        if (url !== $location.path()) {
+          $location.path(url).replace();
+        }
       }
       }
 
 
       if ($routeParams.keepRows) {
       if ($routeParams.keepRows) {

+ 1 - 1
public/app/stores/AlertListStore/AlertListStore.jest.ts

@@ -14,7 +14,7 @@ function getRule(name, state, info) {
       .format(),
       .format(),
     evalData: {},
     evalData: {},
     executionError: '',
     executionError: '',
-    dashboardUri: 'db/mygool',
+    url: 'db/mygool',
     stateText: state,
     stateText: state,
     stateIcon: 'fa',
     stateIcon: 'fa',
     stateClass: 'asd',
     stateClass: 'asd',

+ 1 - 1
public/app/stores/AlertListStore/AlertRule.ts

@@ -13,7 +13,7 @@ export const AlertRule = types
     stateClass: types.string,
     stateClass: types.string,
     stateAge: types.string,
     stateAge: types.string,
     info: types.optional(types.string, ''),
     info: types.optional(types.string, ''),
-    dashboardUri: types.string,
+    url: types.string,
     canEdit: types.boolean,
     canEdit: types.boolean,
   })
   })
   .views(self => ({
   .views(self => ({

+ 9 - 0
public/app/stores/FolderStore/FolderStore.ts

@@ -5,6 +5,7 @@ export const Folder = types.model('Folder', {
   title: types.string,
   title: types.string,
   url: types.string,
   url: types.string,
   canSave: types.boolean,
   canSave: types.boolean,
+  uid: types.string,
   hasChanged: types.boolean,
   hasChanged: types.boolean,
 });
 });
 
 
@@ -14,15 +15,23 @@ export const FolderStore = types
   })
   })
   .actions(self => ({
   .actions(self => ({
     load: flow(function* load(uid: string) {
     load: flow(function* load(uid: string) {
+      // clear folder state
+      if (self.folder && self.folder.uid !== uid) {
+        self.folder = null;
+      }
+
       const backendSrv = getEnv(self).backendSrv;
       const backendSrv = getEnv(self).backendSrv;
       const res = yield backendSrv.getDashboardByUid(uid);
       const res = yield backendSrv.getDashboardByUid(uid);
+
       self.folder = Folder.create({
       self.folder = Folder.create({
         id: res.dashboard.id,
         id: res.dashboard.id,
         title: res.dashboard.title,
         title: res.dashboard.title,
         url: res.meta.url,
         url: res.meta.url,
+        uid: res.dashboard.uid,
         canSave: res.meta.canSave,
         canSave: res.meta.canSave,
         hasChanged: false,
         hasChanged: false,
       });
       });
+
       return res;
       return res;
     }),
     }),
 
 

+ 7 - 3
public/app/stores/PermissionsStore/PermissionsStore.ts

@@ -115,6 +115,7 @@ export const PermissionsStore = types
         self.fetching = false;
         self.fetching = false;
         self.error = null;
         self.error = null;
       }),
       }),
+
       addStoreItem: flow(function* addStoreItem() {
       addStoreItem: flow(function* addStoreItem() {
         self.error = null;
         self.error = null;
         let item = {
         let item = {
@@ -152,11 +153,13 @@ export const PermissionsStore = types
         resetNewType();
         resetNewType();
         return updateItems(self);
         return updateItems(self);
       }),
       }),
+
       removeStoreItem: flow(function* removeStoreItem(idx: number) {
       removeStoreItem: flow(function* removeStoreItem(idx: number) {
         self.error = null;
         self.error = null;
         self.items.splice(idx, 1);
         self.items.splice(idx, 1);
         return updateItems(self);
         return updateItems(self);
       }),
       }),
+
       updatePermissionOnIndex: flow(function* updatePermissionOnIndex(
       updatePermissionOnIndex: flow(function* updatePermissionOnIndex(
         idx: number,
         idx: number,
         permission: number,
         permission: number,
@@ -166,18 +169,19 @@ export const PermissionsStore = types
         self.items[idx].updatePermission(permission, permissionName);
         self.items[idx].updatePermission(permission, permissionName);
         return updateItems(self);
         return updateItems(self);
       }),
       }),
+
       setNewType(newType: string) {
       setNewType(newType: string) {
         self.newItem = NewPermissionsItem.create({ type: newType });
         self.newItem = NewPermissionsItem.create({ type: newType });
       },
       },
+
       resetNewType() {
       resetNewType() {
         resetNewType();
         resetNewType();
       },
       },
+
       toggleAddPermissions() {
       toggleAddPermissions() {
         self.isAddPermissionsVisible = !self.isAddPermissionsVisible;
         self.isAddPermissionsVisible = !self.isAddPermissionsVisible;
       },
       },
-      showAddPermissions() {
-        self.isAddPermissionsVisible = true;
-      },
+
       hideAddPermissions() {
       hideAddPermissions() {
         self.isAddPermissionsVisible = false;
         self.isAddPermissionsVisible = false;
       },
       },

+ 1 - 1
public/sass/components/_dropdown.scss

@@ -255,7 +255,7 @@
 }
 }
 
 
 // Caret to indicate there is a submenu
 // Caret to indicate there is a submenu
-.dropdown-submenu > a::before {
+.dropdown-submenu > a::after {
   display: block;
   display: block;
   content: ' ';
   content: ' ';
   float: right;
   float: right;

+ 1 - 0
public/sass/components/_panel_dashlist.scss

@@ -5,6 +5,7 @@
 
 
 .dashlist-section {
 .dashlist-section {
   margin-bottom: $spacer;
   margin-bottom: $spacer;
+  padding-top: 3px;
 }
 }
 
 
 .dashlist-link {
 .dashlist-link {

+ 6 - 1
public/sass/components/_view_states.scss

@@ -1,5 +1,6 @@
 .page-kiosk-mode {
 .page-kiosk-mode {
-  dashnav {
+  .sidemenu,
+  .navbar {
     display: none;
     display: none;
   }
   }
 }
 }
@@ -31,6 +32,10 @@
     }
     }
   }
   }
 
 
+  .sidemenu {
+    display: none;
+  }
+
   .gf-timepicker-nav-btn {
   .gf-timepicker-nav-btn {
     transform: translate3d(40px, 0, 0);
     transform: translate3d(40px, 0, 0);
   }
   }

+ 0 - 28
public/views/407.html

@@ -1,28 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-  <head>
-    <meta charset="utf-8">
-    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
-    <meta name="viewport" content="width=device-width">
-
-    <title>Grafana</title>
-    <base href="[[.AppSubUrl]]/" />
-
-    <link rel="stylesheet" href="public/build/grafana.dark.min.css" title="Dark">
-    <link rel="icon" type="image/png" href="public/img/fav32.png">
-  </head>
-
-	<body>
-		<div class="gf-box" style="margin: 200px auto 0 auto; width: 500px;">
-			<div class="gf-box-header">
-				<span class="gf-box-title">
-					Proxy authentication required
-				</span>
-			</div>
-
-			<div class="gf-box-body">
-				<h4>Proxy authenticaion required</h4>
-			</div>
-		</div>
-  </body>
-</html>

+ 0 - 39
public/views/500.html

@@ -1,39 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-  <head>
-    <meta charset="utf-8">
-    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
-    <meta name="viewport" content="width=device-width">
-    <title>Grafana - Error</title>
-
-    <base href="[[.AppSubUrl]]/" />
-
-    <link href='public/css/fonts.min.css' rel='stylesheet' type='text/css'>
-		<link rel="stylesheet" href="public/build/grafana.dark.min.css">
-    <link rel="icon" type="image/png" href="public/img/fav32.png">
-  </head>
-
-  <body>
-    <div class="page-container">
-      <div class="page-header">
-        <h1>Server side error :(</h1>
-      </div>
-      <div class="panel-container" style="padding: 2rem">
-        <div class="alert">
-          <div class="alert-icon"><i class="fa fa-exclamation-triangle"></i></div>
-					<div class="alert-body">
-						<div class="alert-title">[[.Title]]</div>
-						<div class="alert-text">
-              [[if .ErrorMsg]]
-                <pre>[[.ErrorMsg]]</pre>
-              [[end]]
-            </div>
-					</div>
-        </div>
-        <div style="padding: 2rem 0 0">
-          <p>Check the Grafana server logs for the detailed error message.</p>
-        </div>
-      </div>
-    </div>
-  </body>
-</html>

+ 57 - 0
public/views/error.html

@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+    <meta name="viewport" content="width=device-width">
+    <meta name="theme-color" content="#000">
+
+    <title>Grafana - Error</title>
+
+    <base href="[[.AppSubUrl]]/" />
+
+    <link rel="stylesheet" href="public/build/grafana.[[ .Theme ]].css?v[[ .BuildVersion ]]">
+
+    <link rel="icon" type="image/png" href="public/img/fav32.png">
+    <link rel="mask-icon" href="public/img/grafana_mask_icon.svg" color="#F05A28">
+
+  </head>
+
+  <body class="theme-[[ .Theme ]]">
+    <div class="main-view">
+      <div class="page-container">
+        <div class="page-header">
+          <div class="page-header__inner">
+            <span class="page-header__logo">
+              <i class="page-header__icon fa fa-frown-o"></i>
+            </span>
+            <div class="page-header__info-block">
+              <h1 class="page-header__title">
+                <a class="text-link" href="login">Grafana</a><span> / Server Error</span><span></span>
+              </h1>
+              <div class="page-header__sub-title">Sadly something went wrong</div>
+            </div>
+          </div>
+        </div>
+      </div>
+      <div class="page-container page-body ng-scope" style="padding: 2rem">
+        <div class="alert">
+          <div class="alert-icon"><i class="fa fa-exclamation-triangle"></i></div>
+          <div class="alert-body">
+            <div class="alert-title">[[.Title]]</div>
+          </div>
+        </div>
+        <br />
+        [[if .ErrorMsg]]
+          <h4 class="page-heading">Error details</h4>
+          <div class="alert-text">
+            <pre>[[.ErrorMsg]]</pre>
+          </div>
+        [[end]]
+        <div style="padding: 2rem 0 0">
+          <p>Check the Grafana server logs for the detailed error message.</p>
+        </div>
+      </div>
+    </div>
+  </body>
+</html>