mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge remote-tracking branch 'upstream/master' into postgres-query-builder
This commit is contained in:
commit
09efcbc205
22
CHANGELOG.md
22
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
|
||||
- **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.
|
||||
- **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)
|
||||
- **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
|
||||
|
||||
@ -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)
|
||||
|
||||
* **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
|
||||
|
||||
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)
|
||||
* **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)
|
||||
* **Cloudwatch**: Fix for multi-valued templated queries. [#9903](https://github.com/grafana/grafana/issues/9903)
|
||||
|
||||
## Tech
|
||||
* **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)
|
||||
|
||||
## Fixes
|
||||
|
@ -28,7 +28,7 @@ in that organization.
|
||||
|
||||
Can do everything scoped to the organization. For example:
|
||||
|
||||
- Add & Edit data data sources.
|
||||
- Add & Edit data sources.
|
||||
- Add & Edit organization users & teams.
|
||||
- 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).
|
||||
|
||||
- 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.
|
||||
|
||||
|
@ -12,6 +12,8 @@ weight = -6
|
||||
|
||||
# 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.
|
||||
|
||||
- [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.
|
||||
- [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.
|
||||
- [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
|
||||
|
||||
<iframe height="215" src="https://www.youtube.com/embed/BC_YRNpqj5k?rel=0&showinfo=0" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>
|
||||
<iframe width="450" height="270" src="https://www.youtube.com/embed/Izr0IBgoTZQ?rel=0&" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>
|
||||
<br />
|
||||
|
||||
## 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
|
||||
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
|
||||
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.
|
||||
|
||||
<div class="clearfix"></div>
|
||||
@ -49,7 +53,7 @@ Almost every page has seen significant UX improvements. All pages (except dashbo
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
### Dashboard Settings
|
||||
## Dashboard Settings
|
||||
|
||||
{{< 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
|
||||
@ -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" >}}
|
||||
|
||||
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>
|
||||
|
||||
@ -78,22 +82,26 @@ which is very useful if you have a lot of dashboards or multiple 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.
|
||||
|
||||
## Permissions
|
||||
|
||||
{{< 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>
|
||||
|
||||
# Provisioning from configuration
|
||||
## Provisioning from configuration
|
||||
|
||||
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
|
||||
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
|
||||
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.
|
||||
@ -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
|
||||
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.
|
||||
|
||||
|
||||
|
@ -62,7 +62,7 @@ Content-Type: application/json
|
||||
}
|
||||
"newStateDate": "2016-12-25",
|
||||
"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",
|
||||
"newStateDate": "2016-12-25",
|
||||
"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
|
||||
|
||||
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`
|
||||
|
||||
@ -294,4 +294,4 @@ Content-Type: application/json
|
||||
{
|
||||
"message": "Notification deleted"
|
||||
}
|
||||
```
|
||||
```
|
||||
|
@ -18,12 +18,15 @@ dashboards, creating users and updating data sources.
|
||||
## 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" >}})
|
||||
|
@ -16,6 +16,7 @@ weight = 1
|
||||
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)
|
||||
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
|
||||
installation.
|
||||
@ -27,6 +28,15 @@ installation.
|
||||
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 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
|
||||
|
||||
|
@ -16,6 +16,7 @@ weight = 2
|
||||
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)
|
||||
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
|
||||
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
|
||||
```
|
||||
|
||||
## 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`.
|
||||
|
||||
#### On CentOS / Fedora / Redhat:
|
||||
|
@ -101,3 +101,8 @@ as this will make upgrades easier without risking losing your config changes.
|
||||
## 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.
|
||||
|
||||
## 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.
|
||||
|
@ -14,6 +14,7 @@ weight = 3
|
||||
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 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
|
||||
installation.
|
||||
|
@ -4,7 +4,7 @@
|
||||
"company": "Grafana Labs"
|
||||
},
|
||||
"name": "grafana",
|
||||
"version": "5.0.0-pre1",
|
||||
"version": "5.0.0-beta1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "http://github.com/grafana/grafana.git"
|
||||
|
@ -1,6 +1,6 @@
|
||||
#! /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
|
||||
|
||||
|
@ -105,7 +105,7 @@ func transformToDTOs(alerts []*models.Alert, c *middleware.Context) ([]*dtos.Ale
|
||||
for _, alert := range alertDTOs {
|
||||
for _, dash := range dashboardsQuery.Result {
|
||||
if alert.DashboardId == dash.Id {
|
||||
alert.DashbboardUri = dash.GenerateUrl()
|
||||
alert.Url = dash.GenerateUrl()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@ -99,7 +99,7 @@ func GetDashboard(c *middleware.Context) Response {
|
||||
return ApiError(500, "Dashboard folder could not be read", err)
|
||||
}
|
||||
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
|
||||
@ -293,10 +293,11 @@ func GetHomeDashboard(c *middleware.Context) Response {
|
||||
}
|
||||
|
||||
if prefsQuery.Result.HomeDashboardId != 0 {
|
||||
slugQuery := m.GetDashboardSlugByIdQuery{Id: prefsQuery.Result.HomeDashboardId}
|
||||
slugQuery := m.GetDashboardRefByIdQuery{Id: prefsQuery.Result.HomeDashboardId}
|
||||
err := bus.Dispatch(&slugQuery)
|
||||
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)
|
||||
} else {
|
||||
log.Warn("Failed to get slug from database, %s", err.Error())
|
||||
|
@ -24,6 +24,12 @@ func GetDashboardAclList(c *middleware.Context) Response {
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,7 @@ type AlertRule struct {
|
||||
EvalDate time.Time `json:"evalDate"`
|
||||
EvalData *simplejson.Json `json:"evalData"`
|
||||
ExecutionError string `json:"executionError"`
|
||||
DashbboardUri string `json:"dashboardUri"`
|
||||
Url string `json:"url"`
|
||||
CanEdit bool `json:"canEdit"`
|
||||
}
|
||||
|
||||
|
@ -27,7 +27,7 @@ type DashboardMeta struct {
|
||||
IsFolder bool `json:"isFolder"`
|
||||
FolderId int64 `json:"folderId"`
|
||||
FolderTitle string `json:"folderTitle"`
|
||||
FolderSlug string `json:"folderSlug"`
|
||||
FolderUrl string `json:"folderUrl"`
|
||||
}
|
||||
|
||||
type DashboardFullWithMeta struct {
|
||||
|
@ -40,14 +40,14 @@ func GenStateString() string {
|
||||
|
||||
func OAuthLogin(ctx *middleware.Context) {
|
||||
if setting.OAuthService == nil {
|
||||
ctx.Handle(404, "login.OAuthLogin(oauth service not enabled)", nil)
|
||||
ctx.Handle(404, "OAuth not enabled", nil)
|
||||
return
|
||||
}
|
||||
|
||||
name := ctx.Params(":name")
|
||||
connect, ok := social.SocialMap[name]
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -42,8 +42,7 @@ func accessForbidden(c *Context) {
|
||||
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) {
|
||||
|
@ -1,6 +1,7 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
@ -24,6 +25,7 @@ func RedirectFromLegacyDashboardUrl() macaron.Handler {
|
||||
|
||||
if slug != "" {
|
||||
if url, err := getDashboardUrlBySlug(c.OrgId, slug); err == nil {
|
||||
url = fmt.Sprintf("%s?%s", url, c.Req.URL.RawQuery)
|
||||
c.Redirect(url, 301)
|
||||
return
|
||||
}
|
||||
@ -38,6 +40,7 @@ func RedirectFromLegacyDashboardSoloUrl() macaron.Handler {
|
||||
if slug != "" {
|
||||
if url, err := getDashboardUrlBySlug(c.OrgId, slug); err == nil {
|
||||
url = strings.Replace(url, "/d/", "/d-solo/", 1)
|
||||
url = fmt.Sprintf("%s?%s", url, c.Req.URL.RawQuery)
|
||||
c.Redirect(url, 301)
|
||||
return
|
||||
}
|
||||
|
@ -30,19 +30,20 @@ func TestMiddlewareDashboardRedirect(t *testing.T) {
|
||||
middlewareScenario("GET dashboard by legacy url", func(sc *scenarioContext) {
|
||||
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() {
|
||||
So(sc.resp.Code, ShouldEqual, 301)
|
||||
redirectUrl, _ := sc.resp.Result().Location()
|
||||
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) {
|
||||
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() {
|
||||
So(sc.resp.Code, ShouldEqual, 301)
|
||||
@ -50,6 +51,7 @@ func TestMiddlewareDashboardRedirect(t *testing.T) {
|
||||
expectedUrl := m.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug)
|
||||
expectedUrl = strings.Replace(expectedUrl, "/d/", "/d-solo/", 1)
|
||||
So(redirectUrl.Path, ShouldEqual, expectedUrl)
|
||||
So(len(redirectUrl.Query()), ShouldEqual, 2)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -206,7 +206,9 @@ func (ctx *Context) Handle(status int, title string, err error) {
|
||||
|
||||
ctx.Data["Title"] = title
|
||||
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) {
|
||||
|
@ -137,7 +137,7 @@ func Recovery() macaron.Handler {
|
||||
|
||||
c.JSON(500, resp)
|
||||
} else {
|
||||
c.HTML(500, "500")
|
||||
c.HTML(500, "error")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
@ -59,6 +59,11 @@ type DashboardAclInfoDTO struct {
|
||||
Role *RoleType `json:"role,omitempty"`
|
||||
Permission PermissionType `json:"permission"`
|
||||
PermissionName string `json:"permissionName"`
|
||||
Uid string `json:"uid"`
|
||||
Title string `json:"title"`
|
||||
Slug string `json:"slug"`
|
||||
IsFolder bool `json:"isFolder"`
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -293,7 +293,7 @@ type DashboardRef struct {
|
||||
Slug string
|
||||
}
|
||||
|
||||
type GetDashboardUIDByIdQuery struct {
|
||||
type GetDashboardRefByIdQuery struct {
|
||||
Id int64
|
||||
Result *DashboardRef
|
||||
}
|
||||
|
@ -90,7 +90,7 @@ func (c *EvalContext) GetDashboardUID() (*m.DashboardRef, error) {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -21,8 +21,9 @@ type Hit struct {
|
||||
Tags []string `json:"tags"`
|
||||
IsStarred bool `json:"isStarred"`
|
||||
FolderId int64 `json:"folderId,omitempty"`
|
||||
FolderUid string `json:"folderUid,omitempty"`
|
||||
FolderTitle string `json:"folderTitle,omitempty"`
|
||||
FolderSlug string `json:"folderSlug,omitempty"`
|
||||
FolderUrl string `json:"folderUrl,omitempty"`
|
||||
}
|
||||
|
||||
type HitList []*Hit
|
||||
|
@ -245,6 +245,7 @@ type DashboardSearchProjection struct {
|
||||
Term string
|
||||
IsFolder bool
|
||||
FolderId int64
|
||||
FolderUid string
|
||||
FolderSlug string
|
||||
FolderTitle string
|
||||
}
|
||||
@ -323,11 +324,15 @@ func makeQueryResult(query *search.FindPersistedDashboardsQuery, res []Dashboard
|
||||
Url: m.GetDashboardFolderUrl(item.IsFolder, item.Uid, item.Slug),
|
||||
Type: getHitType(item),
|
||||
FolderId: item.FolderId,
|
||||
FolderUid: item.FolderUid,
|
||||
FolderTitle: item.FolderTitle,
|
||||
FolderSlug: item.FolderSlug,
|
||||
Tags: []string{},
|
||||
}
|
||||
|
||||
if item.FolderId > 0 {
|
||||
hit.FolderUrl = m.GetFolderUrl(item.FolderUid, item.FolderSlug)
|
||||
}
|
||||
|
||||
query.Result = append(query.Result, hit)
|
||||
hits[item.Id] = hit
|
||||
}
|
||||
@ -569,7 +574,7 @@ func GetDashboardsBySlug(query *m.GetDashboardsBySlugQuery) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetDashboardUIDById(query *m.GetDashboardUIDByIdQuery) error {
|
||||
func GetDashboardUIDById(query *m.GetDashboardRefByIdQuery) error {
|
||||
var rawSql = `SELECT uid, slug from dashboard WHERE Id=?`
|
||||
|
||||
us := &m.DashboardRef{}
|
||||
|
@ -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 {
|
||||
return inTransaction(func(sess *DBSession) error {
|
||||
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 {
|
||||
var err error
|
||||
|
||||
@ -141,7 +147,11 @@ func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error {
|
||||
da.updated,
|
||||
'' as user_login,
|
||||
'' as user_email,
|
||||
'' as team
|
||||
'' as team,
|
||||
'' as title,
|
||||
'' as slug,
|
||||
'' as uid,` +
|
||||
dialect.BooleanStr(false) + ` AS is_folder
|
||||
FROM dashboard_acl as da
|
||||
WHERE da.dashboard_id = -1`
|
||||
query.Result = make([]*m.DashboardAclInfoDTO, 0)
|
||||
@ -155,6 +165,7 @@ func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error {
|
||||
)`, query.DashboardId, query.DashboardId)
|
||||
|
||||
rawSQL := `
|
||||
-- get permissions for the dashboard and its parent folder
|
||||
SELECT
|
||||
da.id,
|
||||
da.org_id,
|
||||
@ -167,13 +178,18 @@ func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error {
|
||||
da.updated,
|
||||
u.login AS user_login,
|
||||
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
|
||||
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 dashboard d on da.dashboard_id = d.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
|
||||
SELECT
|
||||
@ -188,10 +204,14 @@ func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error {
|
||||
da.updated,
|
||||
'' as user_login,
|
||||
'' 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
|
||||
LEFT JOIN dashboard folder on dash.folder_id = folder.id
|
||||
LEFT OUTER JOIN dashboard folder on dash.folder_id = folder.id
|
||||
WHERE
|
||||
dash.id = ? AND (
|
||||
dash.has_acl = ` + dialect.BooleanStr(false) + ` or
|
||||
|
@ -147,6 +147,7 @@ func TestDashboardDataAccess(t *testing.T) {
|
||||
hit := query.Result[0]
|
||||
So(hit.Type, ShouldEqual, search.DashHitFolder)
|
||||
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() {
|
||||
@ -163,6 +164,10 @@ func TestDashboardDataAccess(t *testing.T) {
|
||||
hit := query.Result[0]
|
||||
So(hit.Id, ShouldEqual, savedDash.Id)
|
||||
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() {
|
||||
|
@ -167,4 +167,13 @@ func addDashboardMigration(mg *Migrator) {
|
||||
mg.AddMigration("Remove unique index org_id_slug", NewDropIndexMigration(dashboardV2, &Index{
|
||||
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,
|
||||
}))
|
||||
|
||||
}
|
||||
|
@ -107,6 +107,7 @@ func (sb *SearchBuilder) buildSelect() {
|
||||
dashboard_tag.term,
|
||||
dashboard.is_folder,
|
||||
dashboard.folder_id,
|
||||
folder.uid as folder_uid,
|
||||
folder.slug as folder_slug,
|
||||
folder.title as folder_title
|
||||
FROM `)
|
||||
|
@ -23,7 +23,7 @@ describe('AlertRuleList', () => {
|
||||
.format(),
|
||||
evalData: {},
|
||||
executionError: '',
|
||||
dashboardUri: 'd/ufkcofof/my-goal',
|
||||
url: 'd/ufkcofof/my-goal',
|
||||
canEdit: true,
|
||||
},
|
||||
])
|
||||
|
@ -137,7 +137,7 @@ export class AlertRuleItem extends React.Component<AlertRuleItemProps, any> {
|
||||
'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 (
|
||||
<li className="alert-rule-item">
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import { inject, observer } from 'mobx-react';
|
||||
import { toJS } from 'mobx';
|
||||
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 AddPermissions from 'app/core/components/Permissions/AddPermissions';
|
||||
import SlideDown from 'app/core/components/Animations/SlideDown';
|
||||
|
||||
@inject('nav', 'folder', 'view', 'permissions')
|
||||
@observer
|
||||
export class FolderPermissions extends Component<IContainerProps, any> {
|
||||
@ -17,6 +18,11 @@ export class FolderPermissions extends Component<IContainerProps, any> {
|
||||
this.loadStore();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const { permissions } = this.props;
|
||||
permissions.hideAddPermissions();
|
||||
}
|
||||
|
||||
loadStore() {
|
||||
const { nav, folder, view } = this.props;
|
||||
return folder.load(view.routeParams.get('uid') as string).then(res => {
|
||||
@ -58,7 +64,7 @@ export class FolderPermissions extends Component<IContainerProps, any> {
|
||||
</button>
|
||||
</div>
|
||||
<SlideDown in={permissions.isAddPermissionsVisible}>
|
||||
<AddPermissions permissions={permissions} backendSrv={backendSrv} dashboardId={dashboardId} />
|
||||
<AddPermissions permissions={permissions} backendSrv={backendSrv} />
|
||||
</SlideDown>
|
||||
<Permissions permissions={permissions} isFolder={true} dashboardId={dashboardId} backendSrv={backendSrv} />
|
||||
</div>
|
||||
|
@ -14,6 +14,7 @@ describe('FolderSettings', () => {
|
||||
dashboard: {
|
||||
id: 1,
|
||||
title: 'Folder Name',
|
||||
uid: 'uid-str',
|
||||
},
|
||||
meta: {
|
||||
url: '/dashboards/f/uid/folder-name',
|
||||
@ -23,19 +24,27 @@ describe('FolderSettings', () => {
|
||||
);
|
||||
|
||||
const store = RootStore.create(
|
||||
{},
|
||||
{
|
||||
view: {
|
||||
path: 'asd',
|
||||
query: {},
|
||||
routeParams: {
|
||||
uid: 'uid-str',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
backendSrv: backendSrv,
|
||||
}
|
||||
);
|
||||
|
||||
wrapper = shallow(<FolderSettings backendSrv={backendSrv} {...store} />);
|
||||
return wrapper
|
||||
.dive()
|
||||
page = wrapper.dive();
|
||||
return page
|
||||
.instance()
|
||||
.loadStore()
|
||||
.then(() => {
|
||||
page = wrapper.dive();
|
||||
page.update();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -20,11 +20,5 @@ export function registerAngularDirectives() {
|
||||
['tagOptions', { watchDepth: 'reference' }],
|
||||
]);
|
||||
react2AngularDirective('selectUserPicker', UserPicker, ['backendSrv', 'handlePicked']);
|
||||
react2AngularDirective('dashboardPermissions', DashboardPermissions, [
|
||||
'backendSrv',
|
||||
'dashboardId',
|
||||
'folderTitle',
|
||||
'folderSlug',
|
||||
'folderId',
|
||||
]);
|
||||
react2AngularDirective('dashboardPermissions', DashboardPermissions, ['backendSrv', 'dashboardId', 'folder']);
|
||||
}
|
||||
|
@ -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();
|
||||
return store.permissions.load(1, true, false);
|
||||
});
|
||||
|
@ -9,7 +9,6 @@ import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore'
|
||||
export interface IProps {
|
||||
permissions: any;
|
||||
backendSrv: any;
|
||||
dashboardId: any;
|
||||
}
|
||||
@observer
|
||||
class AddPermissions extends Component<IProps, any> {
|
||||
@ -31,12 +30,6 @@ class AddPermissions extends Component<IProps, any> {
|
||||
const { value } = evt.target;
|
||||
const { permissions } = this.props;
|
||||
|
||||
// if (value === 'Viewer' || value === 'Editor') {
|
||||
// // permissions.addStoreItem({ permission: 1, role: value, dashboardId: dashboardId }, dashboardId);
|
||||
// // this.resetNewType();
|
||||
// return;
|
||||
// }
|
||||
|
||||
permissions.setNewType(value);
|
||||
}
|
||||
|
||||
|
@ -6,12 +6,11 @@ import Tooltip from 'app/core/components/Tooltip/Tooltip';
|
||||
import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo';
|
||||
import AddPermissions from 'app/core/components/Permissions/AddPermissions';
|
||||
import SlideDown from 'app/core/components/Animations/SlideDown';
|
||||
import { FolderInfo } from './FolderInfo';
|
||||
|
||||
export interface IProps {
|
||||
dashboardId: number;
|
||||
folderId: number;
|
||||
folderTitle: string;
|
||||
folderSlug: string;
|
||||
folder?: FolderInfo;
|
||||
backendSrv: any;
|
||||
}
|
||||
@observer
|
||||
@ -28,8 +27,12 @@ class DashboardPermissions extends Component<IProps, any> {
|
||||
this.permissions.toggleAddPermissions();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.permissions.hideAddPermissions();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { dashboardId, folderTitle, folderSlug, folderId, backendSrv } = this.props;
|
||||
const { dashboardId, folder, backendSrv } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@ -50,13 +53,13 @@ class DashboardPermissions extends Component<IProps, any> {
|
||||
</div>
|
||||
</div>
|
||||
<SlideDown in={this.permissions.isAddPermissionsVisible}>
|
||||
<AddPermissions permissions={this.permissions} backendSrv={backendSrv} dashboardId={dashboardId} />
|
||||
<AddPermissions permissions={this.permissions} backendSrv={backendSrv} />
|
||||
</SlideDown>
|
||||
<Permissions
|
||||
permissions={this.permissions}
|
||||
isFolder={false}
|
||||
dashboardId={dashboardId}
|
||||
folderInfo={{ title: folderTitle, slug: folderSlug, id: folderId }}
|
||||
folderInfo={folder}
|
||||
backendSrv={backendSrv}
|
||||
/>
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
export interface FolderInfo {
|
||||
title: string;
|
||||
id: number;
|
||||
slug: string;
|
||||
title: string;
|
||||
url: string;
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ export default observer(({ item, removeItem, permissionChanged, itemIndex, folde
|
||||
folderInfo && (
|
||||
<em className="muted no-wrap">
|
||||
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}
|
||||
</a>{' '}
|
||||
</em>
|
||||
|
@ -7,7 +7,6 @@ export interface Props {
|
||||
}
|
||||
|
||||
export default class ScrollBar extends React.Component<Props, any> {
|
||||
|
||||
private container: any;
|
||||
private ps: PerfectScrollbar;
|
||||
|
||||
@ -16,7 +15,9 @@ export default class ScrollBar extends React.Component<Props, any> {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.ps = new PerfectScrollbar(this.container);
|
||||
this.ps = new PerfectScrollbar(this.container, {
|
||||
wheelPropagation: true,
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
|
@ -83,6 +83,10 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
|
||||
body.toggleClass('sidemenu-hidden');
|
||||
});
|
||||
|
||||
scope.$watch(() => playlistSrv.isPlaying, function(newValue) {
|
||||
elem.toggleClass('playlist-active', newValue === true);
|
||||
});
|
||||
|
||||
// tooltip removal fix
|
||||
// manage page classes
|
||||
var pageClass;
|
||||
|
@ -19,7 +19,6 @@ export class HelpCtrl {
|
||||
],
|
||||
Dashboard: [
|
||||
{ keys: ['mod+s'], description: 'Save dashboard' },
|
||||
{ keys: ['mod+h'], description: 'Hide row controls' },
|
||||
{ keys: ['d', 'r'], description: 'Refresh all panels' },
|
||||
{ keys: ['d', 's'], description: 'Dashboard settings' },
|
||||
{ keys: ['d', 'v'], description: 'Toggle in-active / view mode' },
|
||||
|
@ -1,5 +1,6 @@
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import config from 'app/core/config';
|
||||
|
||||
const template = `
|
||||
<div class="modal-body">
|
||||
@ -60,16 +61,11 @@ export class OrgSwitchCtrl {
|
||||
|
||||
setUsingOrg(org) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,9 @@ export function geminiScrollbar() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function(scope, elem, attrs) {
|
||||
let scrollbar = new PerfectScrollbar(elem[0]);
|
||||
let scrollbar = new PerfectScrollbar(elem[0], {
|
||||
wheelPropagation: true,
|
||||
});
|
||||
let lastPos = 0;
|
||||
|
||||
appEvents.on(
|
||||
|
@ -18,10 +18,6 @@ function (_, $, coreModule) {
|
||||
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) {
|
||||
if (newValue) {
|
||||
elem.toggleClass('dashboard-page--settings-opening', _.isString(newValue));
|
||||
|
@ -150,9 +150,9 @@ export class SearchSrv {
|
||||
if (hit.folderId) {
|
||||
section = {
|
||||
id: hit.folderId,
|
||||
uid: hit.uid,
|
||||
uid: hit.folderUid,
|
||||
title: hit.folderTitle,
|
||||
url: hit.url,
|
||||
url: hit.folderUrl,
|
||||
items: [],
|
||||
icon: 'fa fa-folder-open',
|
||||
toggle: this.toggleFolder.bind(this),
|
||||
|
64
public/app/core/specs/file_export.jest.ts
Normal file
64
public/app/core/specs/file_export.jest.ts
Normal file
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
@ -20,9 +20,6 @@ describe('ManageDashboards', () => {
|
||||
icon: 'fa fa-folder',
|
||||
tags: [],
|
||||
isStarred: false,
|
||||
folderId: 410,
|
||||
folderTitle: 'afolder',
|
||||
folderSlug: 'afolder',
|
||||
},
|
||||
],
|
||||
tags: [],
|
||||
@ -77,9 +74,6 @@ describe('ManageDashboards', () => {
|
||||
icon: 'fa fa-folder',
|
||||
tags: [],
|
||||
isStarred: false,
|
||||
folderId: 410,
|
||||
folderTitle: 'afolder',
|
||||
folderSlug: 'afolder',
|
||||
},
|
||||
],
|
||||
tags: [],
|
||||
@ -112,8 +106,9 @@ describe('ManageDashboards', () => {
|
||||
tags: [],
|
||||
isStarred: false,
|
||||
folderId: 410,
|
||||
folderTitle: 'afolder',
|
||||
folderSlug: 'afolder',
|
||||
folderUid: 'uid',
|
||||
folderTitle: 'Folder',
|
||||
folderUrl: '/dashboards/f/uid/folder',
|
||||
},
|
||||
{
|
||||
id: 500,
|
||||
|
@ -7,6 +7,12 @@ jest.mock('app/core/services/context_srv', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('app/core/config', () => {
|
||||
return {
|
||||
appSubUrl: '/subUrl',
|
||||
};
|
||||
});
|
||||
|
||||
describe('OrgSwitcher', () => {
|
||||
describe('when switching org', () => {
|
||||
let expectedHref;
|
||||
@ -25,8 +31,7 @@ describe('OrgSwitcher', () => {
|
||||
|
||||
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 });
|
||||
});
|
||||
@ -35,8 +40,8 @@ describe('OrgSwitcher', () => {
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -190,7 +190,9 @@ describe('SearchSrv', () => {
|
||||
title: 'dash in folder1 1',
|
||||
type: 'dash-db',
|
||||
folderId: 1,
|
||||
folderUid: 'uid',
|
||||
folderTitle: 'folder1',
|
||||
folderUrl: '/dashboards/f/uid/folder1',
|
||||
},
|
||||
])
|
||||
);
|
||||
@ -206,6 +208,11 @@ describe('SearchSrv', () => {
|
||||
|
||||
it('should group results by folder', () => {
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -3,19 +3,27 @@ import moment from 'moment';
|
||||
import { saveAs } from 'file-saver';
|
||||
|
||||
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';
|
||||
_.each(seriesList, function(series) {
|
||||
_.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');
|
||||
}
|
||||
|
||||
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
|
||||
_.each(seriesList, function(series) {
|
||||
text += series.alias + ';';
|
||||
@ -24,14 +32,15 @@ export function exportSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAUL
|
||||
text += '\n';
|
||||
|
||||
// process data
|
||||
seriesList = mergeSeriesByTime(seriesList);
|
||||
var dataArr = [[]];
|
||||
var sIndex = 1;
|
||||
_.each(seriesList, function(series) {
|
||||
var cIndex = 0;
|
||||
dataArr.push([]);
|
||||
_.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++;
|
||||
});
|
||||
sIndex++;
|
||||
@ -46,6 +55,44 @@ export function exportSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAUL
|
||||
text = text.substring(0, text.length - 1);
|
||||
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');
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { DashboardModel } from './dashboard_model';
|
||||
import locationUtil from 'app/core/utils/location_util';
|
||||
|
||||
export class DashboardSrv {
|
||||
dash: any;
|
||||
@ -74,7 +75,7 @@ export class DashboardSrv {
|
||||
this.dash.version = data.version;
|
||||
|
||||
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);
|
||||
|
@ -93,7 +93,6 @@ export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelP
|
||||
}
|
||||
|
||||
renderPanelItem(panel, index) {
|
||||
console.log('render panel', index);
|
||||
return (
|
||||
<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} />
|
||||
|
@ -4,6 +4,7 @@ import _ from 'lodash';
|
||||
import angular from 'angular';
|
||||
import moment from 'moment';
|
||||
|
||||
import locationUtil from 'app/core/utils/location_util';
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
import { HistoryListOpts, RevisionsModel, CalculateDiffOptions, HistorySrv } from './history_srv';
|
||||
|
||||
@ -185,7 +186,7 @@ export class HistoryListCtrl {
|
||||
return this.historySrv
|
||||
.restoreDashboard(this.dashboard, version)
|
||||
.then(response => {
|
||||
this.$location.path('dashboard/db/' + response.slug);
|
||||
this.$location.url(locationUtil.stripBaseFromUrl(response.url)).replace();
|
||||
this.$route.reload();
|
||||
this.$rootScope.appEvent('alert-success', ['Dashboard restored', 'Restored from version ' + version]);
|
||||
})
|
||||
|
@ -96,13 +96,14 @@
|
||||
</div>
|
||||
|
||||
<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"
|
||||
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 class="dashboard-settings__content" ng-if="ctrl.viewId === '404'">
|
||||
|
@ -14,6 +14,7 @@ export class SettingsCtrl {
|
||||
canSave: boolean;
|
||||
canDelete: boolean;
|
||||
sections: any[];
|
||||
hasUnsavedFolderChange: boolean;
|
||||
|
||||
/** @ngInject */
|
||||
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.appEvent('dash-scroll', { animate: false, pos: 0 });
|
||||
this.$rootScope.onAppEvent('dashboard-saved', this.onPostSave.bind(this), $scope);
|
||||
}
|
||||
|
||||
buildSectionList() {
|
||||
@ -135,6 +137,10 @@ export class SettingsCtrl {
|
||||
this.dashboardSrv.saveDashboard();
|
||||
}
|
||||
|
||||
onPostSave() {
|
||||
this.hasUnsavedFolderChange = false;
|
||||
}
|
||||
|
||||
hideSettings() {
|
||||
var urlParams = this.$location.search();
|
||||
delete urlParams.editview;
|
||||
@ -195,7 +201,15 @@ export class SettingsCtrl {
|
||||
onFolderChange(folder) {
|
||||
this.dashboard.meta.folderId = folder.id;
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -100,7 +100,9 @@ module.directive('grafanaPanel', function($rootScope, $document, $timeout) {
|
||||
// update scrollbar after mounting
|
||||
ctrl.events.on('component-did-mount', () => {
|
||||
if (ctrl.__proto__.constructor.scrollable) {
|
||||
panelScrollbar = new PerfectScrollbar(panelContent[0]);
|
||||
panelScrollbar = new PerfectScrollbar(panelContent[0], {
|
||||
wheelPropagation: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -9,7 +9,7 @@ export class SoloPanelCtrl {
|
||||
|
||||
$scope.init = function() {
|
||||
contextSrv.sidemenu = false;
|
||||
appEvents.emit('toggle-sidemenu');
|
||||
appEvents.emit('toggle-sidemenu-hidden');
|
||||
|
||||
var params = $location.search();
|
||||
panelId = parseInt(params.panelId);
|
||||
|
@ -52,7 +52,7 @@
|
||||
<empty-list-cta model="{
|
||||
title: 'There are no data sources defined yet',
|
||||
buttonIcon: 'gicon gicon-add-datasources',
|
||||
buttonLink: '/datasources/new',
|
||||
buttonLink: 'datasources/new',
|
||||
buttonTitle: 'Add data source',
|
||||
proTip: 'You can also define data sources through configuration files.',
|
||||
proTipLink: 'http://docs.grafana.org/administration/provisioning/#datasources?utm_source=grafana_ds_list',
|
||||
|
@ -12,7 +12,7 @@
|
||||
<div class="alert-rule-item__body">
|
||||
<div class="alert-rule-item__header">
|
||||
<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}}
|
||||
</a>
|
||||
</p>
|
||||
|
@ -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.
|
||||
scrollYMarginOffset: 2,
|
||||
suppressScrollX: true,
|
||||
wheelPropagation: true,
|
||||
};
|
||||
|
||||
if (!legendScrollbar) {
|
||||
|
@ -153,8 +153,12 @@ export class HeatmapTooltip {
|
||||
|
||||
getXBucketIndex(offsetX, data) {
|
||||
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) {
|
||||
|
@ -9,7 +9,7 @@ export class LoadDashboardCtrl {
|
||||
if (!$routeParams.uid && !$routeParams.slug) {
|
||||
backendSrv.get('/api/dashboards/home').then(function(homeDash) {
|
||||
if (homeDash.redirectUri) {
|
||||
$location.path('dashboard/' + homeDash.redirectUri);
|
||||
$location.path(homeDash.redirectUri);
|
||||
} else {
|
||||
var meta = homeDash.meta;
|
||||
meta.canSave = meta.canShare = meta.canStar = false;
|
||||
@ -23,18 +23,19 @@ export class LoadDashboardCtrl {
|
||||
if (!($routeParams.type === 'script' || $routeParams.type === 'snapshot') && !$routeParams.uid) {
|
||||
backendSrv.get(`/api/dashboards/db/${$routeParams.slug}`).then(res => {
|
||||
if (res) {
|
||||
const url = locationUtil.stripBaseFromUrl(res.meta.url);
|
||||
$location.path(url).replace();
|
||||
$location.path(locationUtil.stripBaseFromUrl(res.meta.url)).replace();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -14,7 +14,7 @@ function getRule(name, state, info) {
|
||||
.format(),
|
||||
evalData: {},
|
||||
executionError: '',
|
||||
dashboardUri: 'db/mygool',
|
||||
url: 'db/mygool',
|
||||
stateText: state,
|
||||
stateIcon: 'fa',
|
||||
stateClass: 'asd',
|
||||
|
@ -13,7 +13,7 @@ export const AlertRule = types
|
||||
stateClass: types.string,
|
||||
stateAge: types.string,
|
||||
info: types.optional(types.string, ''),
|
||||
dashboardUri: types.string,
|
||||
url: types.string,
|
||||
canEdit: types.boolean,
|
||||
})
|
||||
.views(self => ({
|
||||
|
@ -5,6 +5,7 @@ export const Folder = types.model('Folder', {
|
||||
title: types.string,
|
||||
url: types.string,
|
||||
canSave: types.boolean,
|
||||
uid: types.string,
|
||||
hasChanged: types.boolean,
|
||||
});
|
||||
|
||||
@ -14,15 +15,23 @@ export const FolderStore = types
|
||||
})
|
||||
.actions(self => ({
|
||||
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 res = yield backendSrv.getDashboardByUid(uid);
|
||||
|
||||
self.folder = Folder.create({
|
||||
id: res.dashboard.id,
|
||||
title: res.dashboard.title,
|
||||
url: res.meta.url,
|
||||
uid: res.dashboard.uid,
|
||||
canSave: res.meta.canSave,
|
||||
hasChanged: false,
|
||||
});
|
||||
|
||||
return res;
|
||||
}),
|
||||
|
||||
|
@ -115,6 +115,7 @@ export const PermissionsStore = types
|
||||
self.fetching = false;
|
||||
self.error = null;
|
||||
}),
|
||||
|
||||
addStoreItem: flow(function* addStoreItem() {
|
||||
self.error = null;
|
||||
let item = {
|
||||
@ -152,11 +153,13 @@ export const PermissionsStore = types
|
||||
resetNewType();
|
||||
return updateItems(self);
|
||||
}),
|
||||
|
||||
removeStoreItem: flow(function* removeStoreItem(idx: number) {
|
||||
self.error = null;
|
||||
self.items.splice(idx, 1);
|
||||
return updateItems(self);
|
||||
}),
|
||||
|
||||
updatePermissionOnIndex: flow(function* updatePermissionOnIndex(
|
||||
idx: number,
|
||||
permission: number,
|
||||
@ -166,18 +169,19 @@ export const PermissionsStore = types
|
||||
self.items[idx].updatePermission(permission, permissionName);
|
||||
return updateItems(self);
|
||||
}),
|
||||
|
||||
setNewType(newType: string) {
|
||||
self.newItem = NewPermissionsItem.create({ type: newType });
|
||||
},
|
||||
|
||||
resetNewType() {
|
||||
resetNewType();
|
||||
},
|
||||
|
||||
toggleAddPermissions() {
|
||||
self.isAddPermissionsVisible = !self.isAddPermissionsVisible;
|
||||
},
|
||||
showAddPermissions() {
|
||||
self.isAddPermissionsVisible = true;
|
||||
},
|
||||
|
||||
hideAddPermissions() {
|
||||
self.isAddPermissionsVisible = false;
|
||||
},
|
||||
|
@ -255,7 +255,7 @@
|
||||
}
|
||||
|
||||
// Caret to indicate there is a submenu
|
||||
.dropdown-submenu > a::before {
|
||||
.dropdown-submenu > a::after {
|
||||
display: block;
|
||||
content: ' ';
|
||||
float: right;
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
.dashlist-section {
|
||||
margin-bottom: $spacer;
|
||||
padding-top: 3px;
|
||||
}
|
||||
|
||||
.dashlist-link {
|
||||
|
@ -1,5 +1,6 @@
|
||||
.page-kiosk-mode {
|
||||
dashnav {
|
||||
.sidemenu,
|
||||
.navbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@ -31,6 +32,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.sidemenu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.gf-timepicker-nav-btn {
|
||||
transform: translate3d(40px, 0, 0);
|
||||
}
|
||||
|
@ -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>
|
@ -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
public/views/error.html
Normal file
57
public/views/error.html
Normal file
@ -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>
|
Loading…
Reference in New Issue
Block a user