mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into 10630_folder_api
This commit is contained in:
commit
cb8c82f7f6
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
|
||||
|
19
README.md
19
README.md
@ -80,8 +80,11 @@ In your custom.ini uncomment (remove the leading `;`) sign. And set `app_mode =
|
||||
|
||||
### Running tests
|
||||
|
||||
- You can run backend Golang tests using "go test ./pkg/...".
|
||||
- Execute all frontend tests with "npm run test"
|
||||
#### Frontend
|
||||
Execute all frontend tests
|
||||
```bash
|
||||
npm run test
|
||||
```
|
||||
|
||||
Writing & watching frontend tests (we have two test runners)
|
||||
|
||||
@ -92,6 +95,18 @@ Writing & watching frontend tests (we have two test runners)
|
||||
- Start watcher: `npm run karma`
|
||||
- Karma+Mocha runs all files that end with the name "_specs.ts".
|
||||
|
||||
#### Backend
|
||||
```bash
|
||||
# Run Golang tests using sqlite3 as database (default)
|
||||
go test ./pkg/...
|
||||
|
||||
# Run Golang tests using mysql as database - convenient to use /docker/blocks/mysql_tests
|
||||
GRAFANA_TEST_DB=mysql go test ./pkg/...
|
||||
|
||||
# Run Golang tests using postgres as database - convenient to use /docker/blocks/postgres_tests
|
||||
GRAFANA_TEST_DB=postgres go test ./pkg/...
|
||||
```
|
||||
|
||||
## Contribute
|
||||
|
||||
If you have any idea for an improvement or found a bug, do not hesitate to open an issue.
|
||||
|
@ -3,4 +3,4 @@
|
||||
# folder: ''
|
||||
# type: file
|
||||
# options:
|
||||
# folder: /var/lib/grafana/dashboards
|
||||
# path: /var/lib/grafana/dashboards
|
||||
|
@ -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" >}})
|
||||
|
@ -1,49 +1,107 @@
|
||||
+++
|
||||
title = "Docs Home"
|
||||
description = "Install guide for Grafana"
|
||||
title = "Grafana documentation"
|
||||
description = "Guides, Installation & Feature Documentation"
|
||||
keywords = ["grafana", "installation", "documentation"]
|
||||
type = "docs"
|
||||
aliases = ["v1.1", "guides/reference/admin"]
|
||||
+++
|
||||
|
||||
# Welcome to the Grafana Documentation
|
||||
# Grafana Documentation
|
||||
|
||||
Grafana is an open source metric analytics & visualization suite. It is most commonly used for
|
||||
visualizing time series data for infrastructure and application analytics but many use it in
|
||||
other domains including industrial sensors, home automation, weather, and process control.
|
||||
<h2>Installing Grafana</h2>
|
||||
<div class="nav-cards">
|
||||
<a href="{{< relref "installation/debian.md" >}}" class="nav-cards__item nav-cards__item--install">
|
||||
<div class="nav-cards__icon fa fa-linux">
|
||||
</div>
|
||||
<h5>Installing on Linux</h5>
|
||||
</a>
|
||||
<a href="{{< relref "installation/mac.md" >}}" class="nav-cards__item nav-cards__item--install">
|
||||
<div class="nav-cards__icon fa fa-apple">
|
||||
</div>
|
||||
<h5>Installing on Mac OS X</h5>
|
||||
</a>
|
||||
<a href="{{< relref "installation/windows.md" >}}" class="nav-cards__item nav-cards__item--install">
|
||||
<div class="nav-cards__icon fa fa-windows">
|
||||
</div>
|
||||
<h5>Installing on Windows</h5>
|
||||
</a>
|
||||
<a href="https://grafana.com/cloud/grafana" class="nav-cards__item nav-cards__item--install">
|
||||
<div class="nav-cards__icon fa fa-cloud">
|
||||
</div>
|
||||
<h5>Grafana Cloud</h5>
|
||||
</a>
|
||||
<a href="https://grafana.com/grafana/download" class="nav-cards__item nav-cards__item--install">
|
||||
<div class="nav-cards__icon fa fa-moon-o">
|
||||
</div>
|
||||
<h5>Nightly Builds</h5>
|
||||
</a>
|
||||
<div class="nav-cards__item nav-cards__item--install">
|
||||
<h5>For other platforms Read the <a href="{{< relref "project/building_from_source.md" >}}">build from source</a>
|
||||
instructions for more information.</h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
## Installing Grafana
|
||||
- [Installing on Debian / Ubuntu](installation/debian)
|
||||
- [Installing on RPM-based Linux (CentOS, Fedora, OpenSuse, RedHat)](installation/rpm)
|
||||
- [Installing on Mac OS X](installation/mac)
|
||||
- [Installing on Windows](installation/windows)
|
||||
- [Installing on Docker](installation/docker)
|
||||
- [Installing using Provisioning (Chef, Puppet, Salt, Ansible, etc)](administration/provisioning#configuration-management-tools)
|
||||
- [Nightly Builds](https://grafana.com/grafana/download)
|
||||
<h2>Guides</h2>
|
||||
|
||||
For other platforms Read the [build from source]({{< relref "project/building_from_source.md" >}})
|
||||
instructions for more information.
|
||||
<div class="nav-cards">
|
||||
<a href="https://grafana.com/grafana" class="nav-cards__item nav-cards__item--guide">
|
||||
<h4>What is Grafana?</h4>
|
||||
<p>Grafana feature highlights.</p>
|
||||
</a>
|
||||
<a href="{{< relref "installation/configuration.md" >}}" class="nav-cards__item nav-cards__item--guide">
|
||||
<h4>Configure Grafana</h4>
|
||||
<p>Article on all the Grafana configuration and setup options.</p>
|
||||
</a>
|
||||
<a href="{{< relref "guides/getting_started.md" >}}" class="nav-cards__item nav-cards__item--guide">
|
||||
<h4>Getting Started</h4>
|
||||
<p>A guide that walks you through the basics of using Grafana</p>
|
||||
</a>
|
||||
<a href="{{< relref "administration/provisioning.md" >}}" class="nav-cards__item nav-cards__item--guide">
|
||||
<h4>Provisioning</h4>
|
||||
<p>A guide to help you automate your Grafana setup & configuration.</p>
|
||||
</a>
|
||||
<a href="{{< relref "guides/whats-new-in-v5.md" >}}" class="nav-cards__item nav-cards__item--guide">
|
||||
<h4>What's new in v5.0</h4>
|
||||
<p>Article on all the new cool features and enhancements in v5.0</p>
|
||||
</a>
|
||||
<a href="{{< relref "tutorials/screencasts.md" >}}" class="nav-cards__item nav-cards__item--guide">
|
||||
<h4>Screencasts</h4>
|
||||
<p>Video tutorials & guides</p>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## Configuring Grafana
|
||||
|
||||
The back-end web server has a number of configuration options. Go the
|
||||
[Configuration]({{< relref "installation/configuration.md" >}}) page for details on all
|
||||
those options.
|
||||
|
||||
|
||||
## Getting Started
|
||||
|
||||
- [Getting Started]({{< relref "guides/getting_started.md" >}})
|
||||
- [Basic Concepts]({{< relref "guides/basic_concepts.md" >}})
|
||||
- [Screencasts]({{< relref "tutorials/screencasts.md" >}})
|
||||
|
||||
## Data Source Guides
|
||||
|
||||
- [Graphite]({{< relref "features/datasources/graphite.md" >}})
|
||||
- [Elasticsearch]({{< relref "features/datasources/elasticsearch.md" >}})
|
||||
- [InfluxDB]({{< relref "features/datasources/influxdb.md" >}})
|
||||
- [Prometheus]({{< relref "features/datasources/prometheus.md" >}})
|
||||
- [OpenTSDB]({{< relref "features/datasources/opentsdb.md" >}})
|
||||
- [MySQL]({{< relref "features/datasources/mysql.md" >}})
|
||||
- [Postgres]({{< relref "features/datasources/postgres.md" >}})
|
||||
- [Cloudwatch]({{< relref "features/datasources/cloudwatch.md" >}})
|
||||
<h2>Data Source Guides</h2>
|
||||
<div class="nav-cards">
|
||||
<a href="{{< relref "features/datasources/graphite.md" >}}" class="nav-cards__item nav-cards__item--ds">
|
||||
<img src="/img/docs/logos/icon_graphite.svg" >
|
||||
<h5>Graphite</h5>
|
||||
</a>
|
||||
<a href="{{< relref "features/datasources/elasticsearch.md" >}}" class="nav-cards__item nav-cards__item--ds">
|
||||
<img src="/img/docs/logos/icon_elasticsearch.svg" >
|
||||
<h5>Elasticsearch</h5>
|
||||
</a>
|
||||
<a href="{{< relref "features/datasources/influxdb.md" >}}" class="nav-cards__item nav-cards__item--ds">
|
||||
<img src="/img/docs/logos/icon_influxdb.svg" >
|
||||
<h5>InfluxDB</h5>
|
||||
</a>
|
||||
<a href="{{< relref "features/datasources/prometheus.md" >}}" class="nav-cards__item nav-cards__item--ds">
|
||||
<img src="/img/docs/logos/icon_prometheus.svg" >
|
||||
<h5>Prometheus</h5>
|
||||
</a>
|
||||
<a href="{{< relref "features/datasources/opentsdb.md" >}}" class="nav-cards__item nav-cards__item--ds">
|
||||
<img src="/img/docs/logos/icon_opentsdb.png" >
|
||||
<h5>OpenTSDB</h5>
|
||||
</a>
|
||||
<a href="{{< relref "features/datasources/mysql.md" >}}" class="nav-cards__item nav-cards__item--ds">
|
||||
<img src="/img/docs/logos/icon_mysql.png" >
|
||||
<h5>MySQL</h5>
|
||||
</a>
|
||||
<a href="{{< relref "features/datasources/postgres.md" >}}" class="nav-cards__item nav-cards__item--ds">
|
||||
<img src="/img/docs/logos/icon_postgres.svg" >
|
||||
<h5>Postgres</h5>
|
||||
</a>
|
||||
<a href="{{< relref "features/datasources/cloudwatch.md" >}}" class="nav-cards__item nav-cards__item--ds">
|
||||
<img src="/img/docs/logos/icon_cloudwatch.svg">
|
||||
<h5>Cloudwatch</h5>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -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.
|
||||
|
@ -84,15 +84,15 @@ An array of:
|
||||
{
|
||||
"target":"upper_75",
|
||||
"datapoints":[
|
||||
[622,1450754160000],
|
||||
[365,1450754220000]
|
||||
[622, 1450754160000],
|
||||
[365, 1450754220000]
|
||||
]
|
||||
},
|
||||
{
|
||||
"target":"upper_90",
|
||||
"datapoints":[
|
||||
[861,1450754160000],
|
||||
[767,1450754220000]
|
||||
[861, 1450754160000],
|
||||
[767, 1450754220000]
|
||||
]
|
||||
}
|
||||
]
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -150,13 +150,13 @@ func (hs *HttpServer) registerRoutes() {
|
||||
apiRoute.Group("/teams", func(teamsRoute RouteRegister) {
|
||||
teamsRoute.Get("/:teamId", wrap(GetTeamById))
|
||||
teamsRoute.Get("/search", wrap(SearchTeams))
|
||||
teamsRoute.Post("/", quota("teams"), reqOrgAdmin, bind(m.CreateTeamCommand{}), wrap(CreateTeam))
|
||||
teamsRoute.Put("/:teamId", reqOrgAdmin, bind(m.UpdateTeamCommand{}), wrap(UpdateTeam))
|
||||
teamsRoute.Delete("/:teamId", reqOrgAdmin, wrap(DeleteTeamById))
|
||||
teamsRoute.Get("/:teamId/members", reqOrgAdmin, wrap(GetTeamMembers))
|
||||
teamsRoute.Post("/:teamId/members", reqOrgAdmin, quota("teams"), bind(m.AddTeamMemberCommand{}), wrap(AddTeamMember))
|
||||
teamsRoute.Delete("/:teamId/members/:userId", reqOrgAdmin, wrap(RemoveTeamMember))
|
||||
})
|
||||
teamsRoute.Post("/", quota("teams"), bind(m.CreateTeamCommand{}), wrap(CreateTeam))
|
||||
teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), wrap(UpdateTeam))
|
||||
teamsRoute.Delete("/:teamId", wrap(DeleteTeamById))
|
||||
teamsRoute.Get("/:teamId/members", wrap(GetTeamMembers))
|
||||
teamsRoute.Post("/:teamId/members", quota("teams"), bind(m.AddTeamMemberCommand{}), wrap(AddTeamMember))
|
||||
teamsRoute.Delete("/:teamId/members/:userId", wrap(RemoveTeamMember))
|
||||
}, reqOrgAdmin)
|
||||
|
||||
// org information available to all users.
|
||||
apiRoute.Group("/org", func(orgRoute RouteRegister) {
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
|
||||
@ -99,7 +100,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
|
||||
@ -218,6 +219,10 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
|
||||
return ApiError(400, m.ErrDashboardTitleEmpty.Error(), nil)
|
||||
}
|
||||
|
||||
if dash.IsFolder && strings.ToLower(dash.Title) == strings.ToLower(m.RootFolderName) {
|
||||
return ApiError(400, "A folder already exists with that name", nil)
|
||||
}
|
||||
|
||||
if dash.Id == 0 {
|
||||
limitReached, err := middleware.QuotaReached(c, "dashboard")
|
||||
if err != nil {
|
||||
@ -228,7 +233,7 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
|
||||
}
|
||||
}
|
||||
|
||||
dashItem := &dashboards.SaveDashboardItem{
|
||||
dashItem := &dashboards.SaveDashboardDTO{
|
||||
Dashboard: dash,
|
||||
Message: cmd.Message,
|
||||
OrgId: c.OrgId,
|
||||
@ -238,8 +243,11 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
|
||||
|
||||
dashboard, err := dashboards.GetRepository().SaveDashboard(dashItem)
|
||||
|
||||
if err == m.ErrDashboardTitleEmpty {
|
||||
return ApiError(400, m.ErrDashboardTitleEmpty.Error(), nil)
|
||||
if err == m.ErrDashboardTitleEmpty ||
|
||||
err == m.ErrDashboardWithSameNameAsFolder ||
|
||||
err == m.ErrDashboardFolderWithSameNameAsDashboard ||
|
||||
err == m.ErrDashboardTypeMismatch {
|
||||
return ApiError(400, err.Error(), nil)
|
||||
}
|
||||
|
||||
if err == m.ErrDashboardContainsInvalidAlertData {
|
||||
@ -294,10 +302,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())
|
||||
|
@ -13,6 +13,11 @@ import (
|
||||
func GetDashboardAclList(c *middleware.Context) Response {
|
||||
dashId := c.ParamsInt64(":dashboardId")
|
||||
|
||||
_, rsp := getDashboardHelper(c.OrgId, "", dashId, "")
|
||||
if rsp != nil {
|
||||
return rsp
|
||||
}
|
||||
|
||||
guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
|
||||
|
||||
if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin {
|
||||
@ -24,12 +29,23 @@ 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)
|
||||
}
|
||||
|
||||
func UpdateDashboardAcl(c *middleware.Context, apiCmd dtos.UpdateDashboardAclCommand) Response {
|
||||
dashId := c.ParamsInt64(":dashboardId")
|
||||
|
||||
_, rsp := getDashboardHelper(c.OrgId, "", dashId, "")
|
||||
if rsp != nil {
|
||||
return rsp
|
||||
}
|
||||
|
||||
guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
|
||||
if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin {
|
||||
return dashboardGuardianResponse(err)
|
||||
@ -73,6 +89,11 @@ func DeleteDashboardAcl(c *middleware.Context) Response {
|
||||
dashId := c.ParamsInt64(":dashboardId")
|
||||
aclId := c.ParamsInt64(":aclId")
|
||||
|
||||
_, rsp := getDashboardHelper(c.OrgId, "", dashId, "")
|
||||
if rsp != nil {
|
||||
return rsp
|
||||
}
|
||||
|
||||
guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
|
||||
if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin {
|
||||
return dashboardGuardianResponse(err)
|
||||
|
@ -23,6 +23,14 @@ func TestDashboardAclApiEndpoint(t *testing.T) {
|
||||
}
|
||||
dtoRes := transformDashboardAclsToDTOs(mockResult)
|
||||
|
||||
getDashboardQueryResult := m.NewDashboard("Dash")
|
||||
var getDashboardNotFoundError error
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
|
||||
query.Result = getDashboardQueryResult
|
||||
return getDashboardNotFoundError
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
|
||||
query.Result = dtoRes
|
||||
return nil
|
||||
@ -60,6 +68,40 @@ func TestDashboardAclApiEndpoint(t *testing.T) {
|
||||
So(respJSON.GetIndex(0).Get("permission").MustInt(), ShouldEqual, m.PERMISSION_VIEW)
|
||||
})
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_ADMIN, func(sc *scenarioContext) {
|
||||
getDashboardNotFoundError = m.ErrDashboardNotFound
|
||||
sc.handlerFunc = GetDashboardAclList
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||
|
||||
Convey("Should not be able to access ACL", func() {
|
||||
So(sc.resp.Code, ShouldEqual, 404)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Should not be able to update permissions for non-existing dashboard", func() {
|
||||
cmd := dtos.UpdateDashboardAclCommand{
|
||||
Items: []dtos.DashboardAclUpdateItem{
|
||||
{UserId: 1000, Permission: m.PERMISSION_ADMIN},
|
||||
},
|
||||
}
|
||||
|
||||
postAclScenario("When calling POST on", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_ADMIN, cmd, func(sc *scenarioContext) {
|
||||
getDashboardNotFoundError = m.ErrDashboardNotFound
|
||||
CallPostAcl(sc)
|
||||
So(sc.resp.Code, ShouldEqual, 404)
|
||||
})
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/2/acl/6", "/api/dashboards/id/:dashboardId/acl/:aclId", m.ROLE_ADMIN, func(sc *scenarioContext) {
|
||||
getDashboardNotFoundError = m.ErrDashboardNotFound
|
||||
sc.handlerFunc = DeleteDashboardAcl
|
||||
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
|
||||
|
||||
Convey("Should not be able to delete non-existing dashboard", func() {
|
||||
So(sc.resp.Code, ShouldEqual, 404)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When user is org editor and has admin permission in the ACL", func() {
|
||||
|
@ -17,15 +17,25 @@ import (
|
||||
)
|
||||
|
||||
type fakeDashboardRepo struct {
|
||||
inserted []*dashboards.SaveDashboardItem
|
||||
inserted []*dashboards.SaveDashboardDTO
|
||||
provisioned []*m.DashboardProvisioning
|
||||
getDashboard []*m.Dashboard
|
||||
}
|
||||
|
||||
func (repo *fakeDashboardRepo) SaveDashboard(json *dashboards.SaveDashboardItem) (*m.Dashboard, error) {
|
||||
func (repo *fakeDashboardRepo) SaveDashboard(json *dashboards.SaveDashboardDTO) (*m.Dashboard, error) {
|
||||
repo.inserted = append(repo.inserted, json)
|
||||
return json.Dashboard, nil
|
||||
}
|
||||
|
||||
func (repo *fakeDashboardRepo) SaveProvisionedDashboard(dto *dashboards.SaveDashboardDTO, provisioning *m.DashboardProvisioning) (*m.Dashboard, error) {
|
||||
repo.inserted = append(repo.inserted, dto)
|
||||
return dto.Dashboard, nil
|
||||
}
|
||||
|
||||
func (repo *fakeDashboardRepo) GetProvisionedDashboardData(name string) ([]*m.DashboardProvisioning, error) {
|
||||
return repo.provisioned, nil
|
||||
}
|
||||
|
||||
var fakeRepo *fakeDashboardRepo
|
||||
|
||||
// This tests two main scenarios. If a user has access to execute an action on a dashboard:
|
||||
|
@ -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 {
|
||||
|
@ -91,13 +91,13 @@ func CreateFolder(c *middleware.Context, cmd m.CreateFolderCommand) Response {
|
||||
return ApiError(403, "Quota reached", nil)
|
||||
}
|
||||
|
||||
dashItem := &dashboards.SaveDashboardItem{
|
||||
saveDashboardDto := &dashboards.SaveDashboardDTO{
|
||||
Dashboard: dashFolder,
|
||||
OrgId: c.OrgId,
|
||||
UserId: c.UserId,
|
||||
}
|
||||
|
||||
folder, err := dashboards.GetRepository().SaveDashboard(dashItem)
|
||||
folder, err := dashboards.GetRepository().SaveDashboard(saveDashboardDto)
|
||||
|
||||
if err != nil {
|
||||
return toFolderError(err)
|
||||
@ -127,14 +127,14 @@ func UpdateFolder(c *middleware.Context, cmd m.UpdateFolderCommand) Response {
|
||||
return ApiError(400, m.ErrFolderTitleEmpty.Error(), nil)
|
||||
}
|
||||
|
||||
dashItem := &dashboards.SaveDashboardItem{
|
||||
saveDashboardDto := &dashboards.SaveDashboardDTO{
|
||||
Dashboard: dashFolder,
|
||||
OrgId: c.OrgId,
|
||||
UserId: c.UserId,
|
||||
Overwrite: cmd.Overwrite,
|
||||
}
|
||||
|
||||
folder, err := dashboards.GetRepository().SaveDashboard(dashItem)
|
||||
folder, err := dashboards.GetRepository().SaveDashboard(saveDashboardDto)
|
||||
|
||||
if err != nil {
|
||||
return toFolderError(err)
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -46,26 +46,30 @@ func addOrgUserHelper(cmd m.AddOrgUserCommand) Response {
|
||||
|
||||
// GET /api/org/users
|
||||
func GetOrgUsersForCurrentOrg(c *middleware.Context) Response {
|
||||
return getOrgUsersHelper(c.OrgId)
|
||||
return getOrgUsersHelper(c.OrgId, c.Params("query"), c.ParamsInt("limit"))
|
||||
}
|
||||
|
||||
// GET /api/orgs/:orgId/users
|
||||
func GetOrgUsers(c *middleware.Context) Response {
|
||||
return getOrgUsersHelper(c.ParamsInt64(":orgId"))
|
||||
return getOrgUsersHelper(c.ParamsInt64(":orgId"), "", 0)
|
||||
}
|
||||
|
||||
func getOrgUsersHelper(orgId int64) Response {
|
||||
query := m.GetOrgUsersQuery{OrgId: orgId}
|
||||
func getOrgUsersHelper(orgId int64, query string, limit int) Response {
|
||||
q := m.GetOrgUsersQuery{
|
||||
OrgId: orgId,
|
||||
Query: query,
|
||||
Limit: limit,
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
if err := bus.Dispatch(&q); err != nil {
|
||||
return ApiError(500, "Failed to get account user", err)
|
||||
}
|
||||
|
||||
for _, user := range query.Result {
|
||||
for _, user := range q.Result {
|
||||
user.AvatarUrl = dtos.GetGravatarUrl(user.Email)
|
||||
}
|
||||
|
||||
return Json(200, query.Result)
|
||||
return Json(200, q.Result)
|
||||
}
|
||||
|
||||
// PATCH /api/org/users/:userId
|
||||
|
@ -26,6 +26,7 @@ func CreateTeam(c *middleware.Context, cmd m.CreateTeamCommand) Response {
|
||||
|
||||
// PUT /api/teams/:teamId
|
||||
func UpdateTeam(c *middleware.Context, cmd m.UpdateTeamCommand) Response {
|
||||
cmd.OrgId = c.OrgId
|
||||
cmd.Id = c.ParamsInt64(":teamId")
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
if err == m.ErrTeamNameTaken {
|
||||
@ -39,7 +40,7 @@ func UpdateTeam(c *middleware.Context, cmd m.UpdateTeamCommand) Response {
|
||||
|
||||
// DELETE /api/teams/:teamId
|
||||
func DeleteTeamById(c *middleware.Context) Response {
|
||||
if err := bus.Dispatch(&m.DeleteTeamCommand{Id: c.ParamsInt64(":teamId")}); err != nil {
|
||||
if err := bus.Dispatch(&m.DeleteTeamCommand{OrgId: c.OrgId, Id: c.ParamsInt64(":teamId")}); err != nil {
|
||||
if err == m.ErrTeamNotFound {
|
||||
return ApiError(404, "Failed to delete Team. ID not found", nil)
|
||||
}
|
||||
@ -60,11 +61,11 @@ func SearchTeams(c *middleware.Context) Response {
|
||||
}
|
||||
|
||||
query := m.SearchTeamsQuery{
|
||||
OrgId: c.OrgId,
|
||||
Query: c.Query("query"),
|
||||
Name: c.Query("name"),
|
||||
Page: page,
|
||||
Limit: perPage,
|
||||
OrgId: c.OrgId,
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
@ -83,7 +84,7 @@ func SearchTeams(c *middleware.Context) Response {
|
||||
|
||||
// GET /api/teams/:teamId
|
||||
func GetTeamById(c *middleware.Context) Response {
|
||||
query := m.GetTeamByIdQuery{Id: c.ParamsInt64(":teamId")}
|
||||
query := m.GetTeamByIdQuery{OrgId: c.OrgId, Id: c.ParamsInt64(":teamId")}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
if err == m.ErrTeamNotFound {
|
||||
|
@ -10,7 +10,7 @@ import (
|
||||
|
||||
// GET /api/teams/:teamId/members
|
||||
func GetTeamMembers(c *middleware.Context) Response {
|
||||
query := m.GetTeamMembersQuery{TeamId: c.ParamsInt64(":teamId")}
|
||||
query := m.GetTeamMembersQuery{OrgId: c.OrgId, TeamId: c.ParamsInt64(":teamId")}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return ApiError(500, "Failed to get Team Members", err)
|
||||
@ -42,7 +42,7 @@ func AddTeamMember(c *middleware.Context, cmd m.AddTeamMemberCommand) Response {
|
||||
|
||||
// DELETE /api/teams/:teamId/members/:userId
|
||||
func RemoveTeamMember(c *middleware.Context) Response {
|
||||
if err := bus.Dispatch(&m.RemoveTeamMemberCommand{TeamId: c.ParamsInt64(":teamId"), UserId: c.ParamsInt64(":userId")}); err != nil {
|
||||
if err := bus.Dispatch(&m.RemoveTeamMemberCommand{OrgId: c.OrgId, TeamId: c.ParamsInt64(":teamId"), UserId: c.ParamsInt64(":userId")}); err != nil {
|
||||
return ApiError(500, "Failed to remove Member from Team", err)
|
||||
}
|
||||
return ApiSuccess("Team Member removed")
|
||||
|
@ -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"`
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -13,17 +13,22 @@ import (
|
||||
|
||||
// Typed errors
|
||||
var (
|
||||
ErrDashboardNotFound = errors.New("Dashboard not found")
|
||||
ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found")
|
||||
ErrDashboardWithSameUIDExists = errors.New("A dashboard with the same uid already exists")
|
||||
ErrDashboardWithSameNameInFolderExists = errors.New("A dashboard with the same name in the folder already exists")
|
||||
ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
|
||||
ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty")
|
||||
ErrDashboardFolderCannotHaveParent = errors.New("A Dashboard Folder cannot be added to another folder")
|
||||
ErrDashboardContainsInvalidAlertData = errors.New("Invalid alert data. Cannot save dashboard")
|
||||
ErrDashboardFailedToUpdateAlertData = errors.New("Failed to save alert data")
|
||||
ErrDashboardsWithSameSlugExists = errors.New("Multiple dashboards with the same slug exists")
|
||||
ErrDashboardFailedGenerateUniqueUid = errors.New("Failed to generate unique dashboard id")
|
||||
ErrDashboardNotFound = errors.New("Dashboard not found")
|
||||
ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found")
|
||||
ErrDashboardWithSameUIDExists = errors.New("A dashboard with the same uid already exists")
|
||||
ErrDashboardWithSameNameInFolderExists = errors.New("A dashboard with the same name in the folder already exists")
|
||||
ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
|
||||
ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty")
|
||||
ErrDashboardFolderCannotHaveParent = errors.New("A Dashboard Folder cannot be added to another folder")
|
||||
ErrDashboardContainsInvalidAlertData = errors.New("Invalid alert data. Cannot save dashboard")
|
||||
ErrDashboardFailedToUpdateAlertData = errors.New("Failed to save alert data")
|
||||
ErrDashboardsWithSameSlugExists = errors.New("Multiple dashboards with the same slug exists")
|
||||
ErrDashboardFailedGenerateUniqueUid = errors.New("Failed to generate unique dashboard id")
|
||||
ErrDashboardExistingCannotChangeToDashboard = errors.New("An existing folder cannot be changed to a dashboard")
|
||||
ErrDashboardTypeMismatch = errors.New("Dashboard cannot be changed to a folder")
|
||||
ErrDashboardFolderWithSameNameAsDashboard = errors.New("Folder name cannot be the same as one of its dashboards")
|
||||
ErrDashboardWithSameNameAsFolder = errors.New("Dashboard name cannot be the same as folder")
|
||||
RootFolderName = "General"
|
||||
)
|
||||
|
||||
type UpdatePluginDashboardError struct {
|
||||
@ -64,6 +69,11 @@ type Dashboard struct {
|
||||
Data *simplejson.Json
|
||||
}
|
||||
|
||||
func (d *Dashboard) SetId(id int64) {
|
||||
d.Id = id
|
||||
d.Data.Set("id", id)
|
||||
}
|
||||
|
||||
// NewDashboard creates a new dashboard
|
||||
func NewDashboard(title string) *Dashboard {
|
||||
dash := &Dashboard{}
|
||||
@ -95,14 +105,21 @@ func NewDashboardFromJson(data *simplejson.Json) *Dashboard {
|
||||
dash.Data = data
|
||||
dash.Title = dash.Data.Get("title").MustString()
|
||||
dash.UpdateSlug()
|
||||
update := false
|
||||
|
||||
if id, err := dash.Data.Get("id").Float64(); err == nil {
|
||||
dash.Id = int64(id)
|
||||
update = true
|
||||
}
|
||||
|
||||
if version, err := dash.Data.Get("version").Float64(); err == nil {
|
||||
dash.Version = int(version)
|
||||
dash.Updated = time.Now()
|
||||
}
|
||||
if uid, err := dash.Data.Get("uid").String(); err == nil {
|
||||
dash.Uid = uid
|
||||
update = true
|
||||
}
|
||||
|
||||
if version, err := dash.Data.Get("version").Float64(); err == nil && update {
|
||||
dash.Version = int(version)
|
||||
dash.Updated = time.Now()
|
||||
} else {
|
||||
dash.Data.Set("version", 0)
|
||||
dash.Created = time.Now()
|
||||
@ -113,10 +130,6 @@ func NewDashboardFromJson(data *simplejson.Json) *Dashboard {
|
||||
dash.GnetId = int64(gnetId)
|
||||
}
|
||||
|
||||
if uid, err := dash.Data.Get("uid").String(); err == nil {
|
||||
dash.Uid = uid
|
||||
}
|
||||
|
||||
return dash
|
||||
}
|
||||
|
||||
@ -211,6 +224,21 @@ type SaveDashboardCommand struct {
|
||||
Result *Dashboard
|
||||
}
|
||||
|
||||
type DashboardProvisioning struct {
|
||||
Id int64
|
||||
DashboardId int64
|
||||
Name string
|
||||
ExternalId string
|
||||
Updated time.Time
|
||||
}
|
||||
|
||||
type SaveProvisionedDashboardCommand struct {
|
||||
DashboardCmd *SaveDashboardCommand
|
||||
DashboardProvisioning *DashboardProvisioning
|
||||
|
||||
Result *Dashboard
|
||||
}
|
||||
|
||||
type DeleteDashboardCommand struct {
|
||||
Id int64
|
||||
OrgId int64
|
||||
@ -263,6 +291,12 @@ type GetDashboardSlugByIdQuery struct {
|
||||
Result string
|
||||
}
|
||||
|
||||
type GetProvisionedDashboardDataQuery struct {
|
||||
Name string
|
||||
|
||||
Result []*DashboardProvisioning
|
||||
}
|
||||
|
||||
type GetDashboardsBySlugQuery struct {
|
||||
OrgId int64
|
||||
Slug string
|
||||
@ -281,7 +315,7 @@ type DashboardRef struct {
|
||||
Slug string
|
||||
}
|
||||
|
||||
type GetDashboardUIDByIdQuery struct {
|
||||
type GetDashboardRefByIdQuery struct {
|
||||
Id int64
|
||||
Result *DashboardRef
|
||||
}
|
||||
|
@ -95,7 +95,10 @@ type UpdateOrgUserCommand struct {
|
||||
// QUERIES
|
||||
|
||||
type GetOrgUsersQuery struct {
|
||||
OrgId int64
|
||||
OrgId int64
|
||||
Query string
|
||||
Limit int
|
||||
|
||||
Result []*OrgUserDTO
|
||||
}
|
||||
|
||||
|
@ -37,18 +37,22 @@ type UpdateTeamCommand struct {
|
||||
Id int64
|
||||
Name string
|
||||
Email string
|
||||
OrgId int64 `json:"-"`
|
||||
}
|
||||
|
||||
type DeleteTeamCommand struct {
|
||||
Id int64
|
||||
OrgId int64
|
||||
Id int64
|
||||
}
|
||||
|
||||
type GetTeamByIdQuery struct {
|
||||
OrgId int64
|
||||
Id int64
|
||||
Result *Team
|
||||
}
|
||||
|
||||
type GetTeamsByUserQuery struct {
|
||||
OrgId int64
|
||||
UserId int64 `json:"userId"`
|
||||
Result []*Team `json:"teams"`
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ type AddTeamMemberCommand struct {
|
||||
}
|
||||
|
||||
type RemoveTeamMemberCommand struct {
|
||||
OrgId int64 `json:"-"`
|
||||
UserId int64
|
||||
TeamId int64
|
||||
}
|
||||
@ -39,6 +40,7 @@ type RemoveTeamMemberCommand struct {
|
||||
// QUERIES
|
||||
|
||||
type GetTeamMembersQuery struct {
|
||||
OrgId int64
|
||||
TeamId int64
|
||||
Result []*TeamMemberDTO
|
||||
}
|
||||
|
@ -82,6 +82,7 @@ func ImportDashboard(cmd *ImportDashboardCommand) error {
|
||||
Path: cmd.Path,
|
||||
Revision: dashboard.Data.Get("revision").MustInt64(1),
|
||||
ImportedUri: "db/" + saveCmd.Result.Slug,
|
||||
ImportedUrl: saveCmd.Result.GetUrl(),
|
||||
ImportedRevision: dashboard.Data.Get("revision").MustInt64(1),
|
||||
Imported: true,
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ type PluginDashboardInfoDTO struct {
|
||||
Title string `json:"title"`
|
||||
Imported bool `json:"imported"`
|
||||
ImportedUri string `json:"importedUri"`
|
||||
ImportedUrl string `json:"importedUrl"`
|
||||
Slug string `json:"slug"`
|
||||
DashboardId int64 `json:"dashboardId"`
|
||||
ImportedRevision int64 `json:"importedRevision"`
|
||||
@ -64,6 +65,7 @@ func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDT
|
||||
res.DashboardId = existingDash.Id
|
||||
res.Imported = true
|
||||
res.ImportedUri = "db/" + existingDash.Slug
|
||||
res.ImportedUrl = existingDash.GetUrl()
|
||||
res.ImportedRevision = existingDash.Data.Get("revision").MustInt64(1)
|
||||
existingMatches[existingDash.Id] = true
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -9,7 +9,9 @@ import (
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
SaveDashboard(*SaveDashboardItem) (*models.Dashboard, error)
|
||||
SaveDashboard(*SaveDashboardDTO) (*models.Dashboard, error)
|
||||
SaveProvisionedDashboard(dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error)
|
||||
GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error)
|
||||
}
|
||||
|
||||
var repositoryInstance Repository
|
||||
@ -22,7 +24,7 @@ func SetRepository(rep Repository) {
|
||||
repositoryInstance = rep
|
||||
}
|
||||
|
||||
type SaveDashboardItem struct {
|
||||
type SaveDashboardDTO struct {
|
||||
OrgId int64
|
||||
UpdatedAt time.Time
|
||||
UserId int64
|
||||
@ -33,15 +35,25 @@ type SaveDashboardItem struct {
|
||||
|
||||
type DashboardRepository struct{}
|
||||
|
||||
func (dr *DashboardRepository) SaveDashboard(json *SaveDashboardItem) (*models.Dashboard, error) {
|
||||
dashboard := json.Dashboard
|
||||
func (dr *DashboardRepository) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) {
|
||||
cmd := &models.GetProvisionedDashboardDataQuery{Name: name}
|
||||
err := bus.Dispatch(cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cmd.Result, nil
|
||||
}
|
||||
|
||||
func (dr *DashboardRepository) buildSaveDashboardCommand(dto *SaveDashboardDTO) (*models.SaveDashboardCommand, error) {
|
||||
dashboard := dto.Dashboard
|
||||
|
||||
if dashboard.Title == "" {
|
||||
return nil, models.ErrDashboardTitleEmpty
|
||||
}
|
||||
|
||||
validateAlertsCmd := alerting.ValidateDashboardAlertsCommand{
|
||||
OrgId: json.OrgId,
|
||||
OrgId: dto.OrgId,
|
||||
Dashboard: dashboard,
|
||||
}
|
||||
|
||||
@ -49,33 +61,77 @@ func (dr *DashboardRepository) SaveDashboard(json *SaveDashboardItem) (*models.D
|
||||
return nil, models.ErrDashboardContainsInvalidAlertData
|
||||
}
|
||||
|
||||
cmd := models.SaveDashboardCommand{
|
||||
cmd := &models.SaveDashboardCommand{
|
||||
Dashboard: dashboard.Data,
|
||||
Message: json.Message,
|
||||
OrgId: json.OrgId,
|
||||
Overwrite: json.Overwrite,
|
||||
UserId: json.UserId,
|
||||
Message: dto.Message,
|
||||
OrgId: dto.OrgId,
|
||||
Overwrite: dto.Overwrite,
|
||||
UserId: dto.UserId,
|
||||
FolderId: dashboard.FolderId,
|
||||
IsFolder: dashboard.IsFolder,
|
||||
}
|
||||
|
||||
if !json.UpdatedAt.IsZero() {
|
||||
cmd.UpdatedAt = json.UpdatedAt
|
||||
if !dto.UpdatedAt.IsZero() {
|
||||
cmd.UpdatedAt = dto.UpdatedAt
|
||||
}
|
||||
|
||||
err := bus.Dispatch(&cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
func (dr *DashboardRepository) updateAlerting(cmd *models.SaveDashboardCommand, dto *SaveDashboardDTO) error {
|
||||
alertCmd := alerting.UpdateDashboardAlertsCommand{
|
||||
OrgId: json.OrgId,
|
||||
UserId: json.UserId,
|
||||
OrgId: dto.OrgId,
|
||||
UserId: dto.UserId,
|
||||
Dashboard: cmd.Result,
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&alertCmd); err != nil {
|
||||
return nil, models.ErrDashboardFailedToUpdateAlertData
|
||||
return models.ErrDashboardFailedToUpdateAlertData
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dr *DashboardRepository) SaveProvisionedDashboard(dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) {
|
||||
cmd, err := dr.buildSaveDashboardCommand(dto)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
saveCmd := &models.SaveProvisionedDashboardCommand{
|
||||
DashboardCmd: cmd,
|
||||
DashboardProvisioning: provisioning,
|
||||
}
|
||||
|
||||
// dashboard
|
||||
err = bus.Dispatch(saveCmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//alerts
|
||||
err = dr.updateAlerting(cmd, dto)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cmd.Result, nil
|
||||
}
|
||||
|
||||
func (dr *DashboardRepository) SaveDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
|
||||
cmd, err := dr.buildSaveDashboardCommand(dto)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = bus.Dispatch(cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = dr.updateAlerting(cmd, dto)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cmd.Result, nil
|
||||
|
@ -160,7 +160,7 @@ func (g *DashboardGuardian) getTeams() ([]*m.Team, error) {
|
||||
return g.groups, nil
|
||||
}
|
||||
|
||||
query := m.GetTeamsByUserQuery{UserId: g.user.UserId}
|
||||
query := m.GetTeamsByUserQuery{OrgId: g.orgId, UserId: g.user.UserId}
|
||||
err := bus.Dispatch(&query)
|
||||
|
||||
g.groups = query.Result
|
||||
|
@ -1,33 +0,0 @@
|
||||
package dashboards
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
gocache "github.com/patrickmn/go-cache"
|
||||
"time"
|
||||
)
|
||||
|
||||
type dashboardCache struct {
|
||||
internalCache *gocache.Cache
|
||||
}
|
||||
|
||||
func NewDashboardCache() *dashboardCache {
|
||||
return &dashboardCache{internalCache: gocache.New(5*time.Minute, 30*time.Minute)}
|
||||
}
|
||||
|
||||
func (fr *dashboardCache) addDashboardCache(key string, json *dashboards.SaveDashboardItem) {
|
||||
fr.internalCache.Add(key, json, time.Minute*10)
|
||||
}
|
||||
|
||||
func (fr *dashboardCache) getCache(key string) (*dashboards.SaveDashboardItem, bool) {
|
||||
obj, exist := fr.internalCache.Get(key)
|
||||
if !exist {
|
||||
return nil, exist
|
||||
}
|
||||
|
||||
dash, ok := obj.(*dashboards.SaveDashboardItem)
|
||||
if !ok {
|
||||
return nil, ok
|
||||
}
|
||||
|
||||
return dash, ok
|
||||
}
|
@ -29,8 +29,6 @@ type fileReader struct {
|
||||
Path string
|
||||
log log.Logger
|
||||
dashboardRepo dashboards.Repository
|
||||
cache *dashboardCache
|
||||
createWalk func(fr *fileReader, folderId int64) filepath.WalkFunc
|
||||
}
|
||||
|
||||
func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReader, error) {
|
||||
@ -54,24 +52,22 @@ func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReade
|
||||
Path: path,
|
||||
log: log,
|
||||
dashboardRepo: dashboards.GetRepository(),
|
||||
cache: NewDashboardCache(),
|
||||
createWalk: createWalkFn,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (fr *fileReader) ReadAndListen(ctx context.Context) error {
|
||||
ticker := time.NewTicker(checkDiskForChangesInterval)
|
||||
|
||||
if err := fr.startWalkingDisk(); err != nil {
|
||||
fr.log.Error("failed to search for dashboards", "error", err)
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(checkDiskForChangesInterval)
|
||||
|
||||
running := false
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if !running { // avoid walking the filesystem in parallel. incase fs is very slow.
|
||||
if !running { // avoid walking the filesystem in parallel. in-case fs is very slow.
|
||||
running = true
|
||||
go func() {
|
||||
if err := fr.startWalkingDisk(); err != nil {
|
||||
@ -98,7 +94,91 @@ func (fr *fileReader) startWalkingDisk() error {
|
||||
return err
|
||||
}
|
||||
|
||||
return filepath.Walk(fr.Path, fr.createWalk(fr, folderId))
|
||||
provisionedDashboardRefs, err := getProvisionedDashboardByPath(fr.dashboardRepo, fr.Cfg.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filesFoundOnDisk := map[string]os.FileInfo{}
|
||||
err = filepath.Walk(fr.Path, createWalkFn(filesFoundOnDisk))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// find dashboards to delete since json file is missing
|
||||
var dashboardToDelete []int64
|
||||
for path, provisioningData := range provisionedDashboardRefs {
|
||||
_, existsOnDisk := filesFoundOnDisk[path]
|
||||
if !existsOnDisk {
|
||||
dashboardToDelete = append(dashboardToDelete, provisioningData.DashboardId)
|
||||
}
|
||||
}
|
||||
|
||||
// delete dashboard that are missing json file
|
||||
for _, dashboardId := range dashboardToDelete {
|
||||
fr.log.Debug("deleting provisioned dashboard. missing on disk", "id", dashboardId)
|
||||
cmd := &models.DeleteDashboardCommand{OrgId: fr.Cfg.OrgId, Id: dashboardId}
|
||||
err := bus.Dispatch(cmd)
|
||||
if err != nil {
|
||||
fr.log.Error("failed to delete dashboard", "id", cmd.Id)
|
||||
}
|
||||
}
|
||||
|
||||
// save dashboards based on json files
|
||||
for path, fileInfo := range filesFoundOnDisk {
|
||||
err = fr.saveDashboard(path, folderId, fileInfo, provisionedDashboardRefs)
|
||||
if err != nil {
|
||||
fr.log.Error("failed to save dashboard", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fr *fileReader) saveDashboard(path string, folderId int64, fileInfo os.FileInfo, provisionedDashboardRefs map[string]*models.DashboardProvisioning) error {
|
||||
resolvedFileInfo, err := resolveSymlink(fileInfo, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
provisionedData, alreadyProvisioned := provisionedDashboardRefs[path]
|
||||
if alreadyProvisioned && provisionedData.Updated.Unix() == resolvedFileInfo.ModTime().Unix() {
|
||||
return nil // dashboard is already in sync with the database
|
||||
}
|
||||
|
||||
dash, err := fr.readDashboardFromFile(path, resolvedFileInfo.ModTime(), folderId)
|
||||
if err != nil {
|
||||
fr.log.Error("failed to load dashboard from ", "file", path, "error", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if dash.Dashboard.Id != 0 {
|
||||
fr.log.Error("provisioned dashboard json files cannot contain id")
|
||||
return nil
|
||||
}
|
||||
|
||||
if alreadyProvisioned {
|
||||
dash.Dashboard.SetId(provisionedData.DashboardId)
|
||||
}
|
||||
|
||||
fr.log.Debug("saving new dashboard", "file", path)
|
||||
dp := &models.DashboardProvisioning{ExternalId: path, Name: fr.Cfg.Name, Updated: resolvedFileInfo.ModTime()}
|
||||
_, err = fr.dashboardRepo.SaveProvisionedDashboard(dash, dp)
|
||||
return err
|
||||
}
|
||||
|
||||
func getProvisionedDashboardByPath(repo dashboards.Repository, name string) (map[string]*models.DashboardProvisioning, error) {
|
||||
arr, err := repo.GetProvisionedDashboardData(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
byPath := map[string]*models.DashboardProvisioning{}
|
||||
for _, pd := range arr {
|
||||
byPath[pd.ExternalId] = pd
|
||||
}
|
||||
|
||||
return byPath, nil
|
||||
}
|
||||
|
||||
func getOrCreateFolderId(cfg *DashboardsAsConfig, repo dashboards.Repository) (int64, error) {
|
||||
@ -115,7 +195,7 @@ func getOrCreateFolderId(cfg *DashboardsAsConfig, repo dashboards.Repository) (i
|
||||
|
||||
// dashboard folder not found. create one.
|
||||
if err == models.ErrDashboardNotFound {
|
||||
dash := &dashboards.SaveDashboardItem{}
|
||||
dash := &dashboards.SaveDashboardDTO{}
|
||||
dash.Dashboard = models.NewDashboard(cfg.Folder)
|
||||
dash.Dashboard.IsFolder = true
|
||||
dash.Overwrite = true
|
||||
@ -129,83 +209,59 @@ func getOrCreateFolderId(cfg *DashboardsAsConfig, repo dashboards.Repository) (i
|
||||
}
|
||||
|
||||
if !cmd.Result.IsFolder {
|
||||
return 0, fmt.Errorf("Got invalid response. Expected folder, found dashboard")
|
||||
return 0, fmt.Errorf("got invalid response. expected folder, found dashboard")
|
||||
}
|
||||
|
||||
return cmd.Result.Id, nil
|
||||
}
|
||||
|
||||
func createWalkFn(fr *fileReader, folderId int64) filepath.WalkFunc {
|
||||
func resolveSymlink(fileinfo os.FileInfo, path string) (os.FileInfo, error) {
|
||||
checkFilepath, err := filepath.EvalSymlinks(path)
|
||||
if path != checkFilepath {
|
||||
path = checkFilepath
|
||||
fi, err := os.Lstat(checkFilepath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return fi, nil
|
||||
}
|
||||
|
||||
return fileinfo, err
|
||||
}
|
||||
|
||||
func createWalkFn(filesOnDisk map[string]os.FileInfo) filepath.WalkFunc {
|
||||
return func(path string, fileInfo os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if fileInfo.IsDir() {
|
||||
if strings.HasPrefix(fileInfo.Name(), ".") {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(fileInfo.Name(), ".json") {
|
||||
return nil
|
||||
}
|
||||
|
||||
checkFilepath, err := filepath.EvalSymlinks(path)
|
||||
|
||||
if path != checkFilepath {
|
||||
path = checkFilepath
|
||||
fi, err := os.Lstat(checkFilepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fileInfo = fi
|
||||
}
|
||||
|
||||
cachedDashboard, exist := fr.cache.getCache(path)
|
||||
if exist && cachedDashboard.UpdatedAt == fileInfo.ModTime() {
|
||||
return nil
|
||||
}
|
||||
|
||||
dash, err := fr.readDashboardFromFile(path, folderId)
|
||||
if err != nil {
|
||||
fr.log.Error("failed to load dashboard from ", "file", path, "error", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if dash.Dashboard.Id != 0 {
|
||||
fr.log.Error("Cannot provision dashboard. Please remove the id property from the json file")
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd := &models.GetDashboardQuery{Slug: dash.Dashboard.Slug}
|
||||
err = bus.Dispatch(cmd)
|
||||
|
||||
// if we don't have the dashboard in the db, save it!
|
||||
if err == models.ErrDashboardNotFound {
|
||||
fr.log.Debug("saving new dashboard", "file", path)
|
||||
_, err = fr.dashboardRepo.SaveDashboard(dash)
|
||||
isValid, err := validateWalkablePath(fileInfo)
|
||||
if !isValid {
|
||||
return err
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fr.log.Error("failed to query for dashboard", "slug", dash.Dashboard.Slug, "error", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// break if db version is newer then fil version
|
||||
if cmd.Result.Updated.Unix() >= fileInfo.ModTime().Unix() {
|
||||
return nil
|
||||
}
|
||||
|
||||
fr.log.Debug("loading dashboard from disk into database.", "file", path)
|
||||
_, err = fr.dashboardRepo.SaveDashboard(dash)
|
||||
|
||||
return err
|
||||
filesOnDisk[path] = fileInfo
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (fr *fileReader) readDashboardFromFile(path string, folderId int64) (*dashboards.SaveDashboardItem, error) {
|
||||
func validateWalkablePath(fileInfo os.FileInfo) (bool, error) {
|
||||
if fileInfo.IsDir() {
|
||||
if strings.HasPrefix(fileInfo.Name(), ".") {
|
||||
return false, filepath.SkipDir
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(fileInfo.Name(), ".json") {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (fr *fileReader) readDashboardFromFile(path string, lastModified time.Time, folderId int64) (*dashboards.SaveDashboardDTO, error) {
|
||||
reader, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -217,17 +273,10 @@ func (fr *fileReader) readDashboardFromFile(path string, folderId int64) (*dashb
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stat, err := os.Stat(path)
|
||||
dash, err := createDashboardJson(data, lastModified, fr.Cfg, folderId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dash, err := createDashboardJson(data, stat.ModTime(), fr.Cfg, folderId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fr.cache.addDashboardCache(path, dash)
|
||||
|
||||
return dash, nil
|
||||
}
|
||||
|
@ -62,25 +62,8 @@ func TestDashboardFileReader(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
So(dashboards, ShouldEqual, 2)
|
||||
So(folders, ShouldEqual, 1)
|
||||
})
|
||||
|
||||
Convey("Should not update dashboards when db is newer", func() {
|
||||
cfg.Options["path"] = oneDashboard
|
||||
|
||||
fakeRepo.getDashboard = append(fakeRepo.getDashboard, &models.Dashboard{
|
||||
Updated: time.Now().Add(time.Hour),
|
||||
Slug: "grafana",
|
||||
})
|
||||
|
||||
reader, err := NewDashboardFileReader(cfg, logger)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
err = reader.startWalkingDisk()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(len(fakeRepo.inserted), ShouldEqual, 0)
|
||||
So(dashboards, ShouldEqual, 2)
|
||||
})
|
||||
|
||||
Convey("Can read default dashboard and replace old version in database", func() {
|
||||
@ -161,26 +144,15 @@ func TestDashboardFileReader(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Walking the folder with dashboards", func() {
|
||||
cfg := &DashboardsAsConfig{
|
||||
Name: "Default",
|
||||
Type: "file",
|
||||
OrgId: 1,
|
||||
Folder: "",
|
||||
Options: map[string]interface{}{
|
||||
"path": defaultDashboards,
|
||||
},
|
||||
}
|
||||
|
||||
reader, err := NewDashboardFileReader(cfg, log.New("test-logger"))
|
||||
So(err, ShouldBeNil)
|
||||
noFiles := map[string]os.FileInfo{}
|
||||
|
||||
Convey("should skip dirs that starts with .", func() {
|
||||
shouldSkip := reader.createWalk(reader, 0)("path", &FakeFileInfo{isDirectory: true, name: ".folder"}, nil)
|
||||
shouldSkip := createWalkFn(noFiles)("path", &FakeFileInfo{isDirectory: true, name: ".folder"}, nil)
|
||||
So(shouldSkip, ShouldEqual, filepath.SkipDir)
|
||||
})
|
||||
|
||||
Convey("should keep walking if file is not .json", func() {
|
||||
shouldSkip := reader.createWalk(reader, 0)("path", &FakeFileInfo{isDirectory: true, name: "folder"}, nil)
|
||||
shouldSkip := createWalkFn(noFiles)("path", &FakeFileInfo{isDirectory: true, name: "folder"}, nil)
|
||||
So(shouldSkip, ShouldBeNil)
|
||||
})
|
||||
})
|
||||
@ -241,15 +213,26 @@ func (ffi FakeFileInfo) Sys() interface{} {
|
||||
}
|
||||
|
||||
type fakeDashboardRepo struct {
|
||||
inserted []*dashboards.SaveDashboardItem
|
||||
inserted []*dashboards.SaveDashboardDTO
|
||||
provisioned []*models.DashboardProvisioning
|
||||
getDashboard []*models.Dashboard
|
||||
}
|
||||
|
||||
func (repo *fakeDashboardRepo) SaveDashboard(json *dashboards.SaveDashboardItem) (*models.Dashboard, error) {
|
||||
func (repo *fakeDashboardRepo) SaveDashboard(json *dashboards.SaveDashboardDTO) (*models.Dashboard, error) {
|
||||
repo.inserted = append(repo.inserted, json)
|
||||
return json.Dashboard, nil
|
||||
}
|
||||
|
||||
func (repo *fakeDashboardRepo) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) {
|
||||
return repo.provisioned, nil
|
||||
}
|
||||
|
||||
func (repo *fakeDashboardRepo) SaveProvisionedDashboard(dto *dashboards.SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) {
|
||||
repo.inserted = append(repo.inserted, dto)
|
||||
repo.provisioned = append(repo.provisioned, provisioning)
|
||||
return dto.Dashboard, nil
|
||||
}
|
||||
|
||||
func mockGetDashboardQuery(cmd *models.GetDashboardQuery) error {
|
||||
for _, d := range fakeRepo.getDashboard {
|
||||
if d.Slug == cmd.Slug {
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"title": "Grafana",
|
||||
"title": "Grafana1",
|
||||
"tags": [],
|
||||
"style": "dark",
|
||||
"timezone": "browser",
|
||||
@ -170,4 +170,3 @@
|
||||
},
|
||||
"version": 5
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"title": "Grafana",
|
||||
"title": "Grafana2",
|
||||
"tags": [],
|
||||
"style": "dark",
|
||||
"timezone": "browser",
|
||||
@ -170,4 +170,3 @@
|
||||
},
|
||||
"version": 5
|
||||
}
|
||||
|
@ -18,8 +18,8 @@ type DashboardsAsConfig struct {
|
||||
Options map[string]interface{} `json:"options" yaml:"options"`
|
||||
}
|
||||
|
||||
func createDashboardJson(data *simplejson.Json, lastModified time.Time, cfg *DashboardsAsConfig, folderId int64) (*dashboards.SaveDashboardItem, error) {
|
||||
dash := &dashboards.SaveDashboardItem{}
|
||||
func createDashboardJson(data *simplejson.Json, lastModified time.Time, cfg *DashboardsAsConfig, folderId int64) (*dashboards.SaveDashboardDTO, error) {
|
||||
dash := &dashboards.SaveDashboardDTO{}
|
||||
dash.Dashboard = models.NewDashboardFromJson(data)
|
||||
dash.UpdatedAt = lastModified
|
||||
dash.Overwrite = true
|
||||
|
@ -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
|
||||
|
@ -30,143 +30,198 @@ var generateNewUid func() string = util.GenerateShortUid
|
||||
|
||||
func SaveDashboard(cmd *m.SaveDashboardCommand) error {
|
||||
return inTransaction(func(sess *DBSession) error {
|
||||
dash := cmd.GetDashboardModel()
|
||||
return saveDashboard(sess, cmd)
|
||||
})
|
||||
}
|
||||
|
||||
// try get existing dashboard
|
||||
var existing m.Dashboard
|
||||
func saveDashboard(sess *DBSession, cmd *m.SaveDashboardCommand) error {
|
||||
dash := cmd.GetDashboardModel()
|
||||
|
||||
if dash.Id != 0 {
|
||||
dashWithIdExists, err := sess.Where("id=? AND org_id=?", dash.Id, dash.OrgId).Get(&existing)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !dashWithIdExists {
|
||||
return m.ErrDashboardNotFound
|
||||
if err := getExistingDashboardForUpdate(sess, dash, cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var existingByTitleAndFolder m.Dashboard
|
||||
|
||||
dashWithTitleAndFolderExists, err := sess.Where("org_id=? AND slug=? AND (is_folder=? OR folder_id=?)", dash.OrgId, dash.Slug, dialect.BooleanStr(true), dash.FolderId).Get(&existingByTitleAndFolder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if dashWithTitleAndFolderExists {
|
||||
if dash.Id != existingByTitleAndFolder.Id {
|
||||
if existingByTitleAndFolder.IsFolder && !cmd.IsFolder {
|
||||
return m.ErrDashboardWithSameNameAsFolder
|
||||
}
|
||||
|
||||
// check for is someone else has written in between
|
||||
if dash.Version != existing.Version {
|
||||
if cmd.Overwrite {
|
||||
dash.Version = existing.Version
|
||||
} else {
|
||||
return m.ErrDashboardVersionMismatch
|
||||
}
|
||||
}
|
||||
|
||||
// do not allow plugin dashboard updates without overwrite flag
|
||||
if existing.PluginId != "" && cmd.Overwrite == false {
|
||||
return m.UpdatePluginDashboardError{PluginId: existing.PluginId}
|
||||
}
|
||||
|
||||
dash.Created = existing.Created
|
||||
dash.CreatedBy = existing.CreatedBy
|
||||
} else if dash.Uid != "" {
|
||||
var sameUid m.Dashboard
|
||||
sameUidExists, err := sess.Where("org_id=? AND uid=?", dash.OrgId, dash.Uid).Get(&sameUid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if sameUidExists {
|
||||
// another dashboard with same uid
|
||||
if dash.Id != sameUid.Id {
|
||||
if cmd.Overwrite {
|
||||
dash.Id = sameUid.Id
|
||||
dash.Version = sameUid.Version
|
||||
} else {
|
||||
return m.ErrDashboardWithSameUIDExists
|
||||
}
|
||||
} else {
|
||||
dash.Created = sameUid.Created
|
||||
dash.CreatedBy = sameUid.CreatedBy
|
||||
if !existingByTitleAndFolder.IsFolder && cmd.IsFolder {
|
||||
return m.ErrDashboardFolderWithSameNameAsDashboard
|
||||
}
|
||||
|
||||
if cmd.Overwrite {
|
||||
dash.Id = existingByTitleAndFolder.Id
|
||||
dash.Created = existingByTitleAndFolder.Created
|
||||
dash.CreatedBy = existingByTitleAndFolder.CreatedBy
|
||||
dash.Version = existingByTitleAndFolder.Version
|
||||
|
||||
if dash.Uid == "" {
|
||||
dash.Uid = existingByTitleAndFolder.Uid
|
||||
}
|
||||
} else {
|
||||
return m.ErrDashboardWithSameNameInFolderExists
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if dash.Uid == "" {
|
||||
uid, err := generateNewDashboardUid(sess, dash.OrgId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dash.Uid = uid
|
||||
dash.Data.Set("uid", uid)
|
||||
}
|
||||
|
||||
err = setHasAcl(sess, dash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
parentVersion := dash.Version
|
||||
affectedRows := int64(0)
|
||||
|
||||
if dash.Id == 0 {
|
||||
dash.Version = 1
|
||||
metrics.M_Api_Dashboard_Insert.Inc()
|
||||
dash.Data.Set("version", dash.Version)
|
||||
affectedRows, err = sess.Insert(dash)
|
||||
} else {
|
||||
dash.Version++
|
||||
dash.Data.Set("version", dash.Version)
|
||||
|
||||
if !cmd.UpdatedAt.IsZero() {
|
||||
dash.Updated = cmd.UpdatedAt
|
||||
}
|
||||
|
||||
affectedRows, err = sess.MustCols("folder_id", "has_acl").ID(dash.Id).Update(dash)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if affectedRows == 0 {
|
||||
return m.ErrDashboardNotFound
|
||||
}
|
||||
|
||||
dashVersion := &m.DashboardVersion{
|
||||
DashboardId: dash.Id,
|
||||
ParentVersion: parentVersion,
|
||||
RestoredFrom: cmd.RestoredFrom,
|
||||
Version: dash.Version,
|
||||
Created: time.Now(),
|
||||
CreatedBy: dash.UpdatedBy,
|
||||
Message: cmd.Message,
|
||||
Data: dash.Data,
|
||||
}
|
||||
|
||||
// insert version entry
|
||||
if affectedRows, err = sess.Insert(dashVersion); err != nil {
|
||||
return err
|
||||
} else if affectedRows == 0 {
|
||||
return m.ErrDashboardNotFound
|
||||
}
|
||||
|
||||
// delete existing tags
|
||||
_, err = sess.Exec("DELETE FROM dashboard_tag WHERE dashboard_id=?", dash.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// insert new tags
|
||||
tags := dash.GetTags()
|
||||
if len(tags) > 0 {
|
||||
for _, tag := range tags {
|
||||
if _, err := sess.Insert(&DashboardTag{DashboardId: dash.Id, Term: tag}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cmd.Result = dash
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func getExistingDashboardForUpdate(sess *DBSession, dash *m.Dashboard, cmd *m.SaveDashboardCommand) (err error) {
|
||||
dashWithIdExists := false
|
||||
var existingById m.Dashboard
|
||||
|
||||
if dash.Id > 0 {
|
||||
dashWithIdExists, err = sess.Where("id=? AND org_id=?", dash.Id, dash.OrgId).Get(&existingById)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !dashWithIdExists {
|
||||
return m.ErrDashboardNotFound
|
||||
}
|
||||
|
||||
dash.Created = existingById.Created
|
||||
dash.CreatedBy = existingById.CreatedBy
|
||||
|
||||
if dash.Uid == "" {
|
||||
uid, err := generateNewDashboardUid(sess, dash.OrgId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dash.Uid = uid
|
||||
dash.Data.Set("uid", uid)
|
||||
dash.Uid = existingById.Uid
|
||||
}
|
||||
}
|
||||
|
||||
err := guaranteeDashboardNameIsUniqueInFolder(sess, dash)
|
||||
dashWithUidExists := false
|
||||
var existingByUid m.Dashboard
|
||||
|
||||
if dash.Uid != "" {
|
||||
dashWithUidExists, err = sess.Where("org_id=? AND uid=?", dash.OrgId, dash.Uid).Get(&existingByUid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = setHasAcl(sess, dash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !dashWithIdExists && !dashWithUidExists {
|
||||
return nil
|
||||
}
|
||||
|
||||
parentVersion := dash.Version
|
||||
affectedRows := int64(0)
|
||||
if dashWithIdExists && dashWithUidExists && existingById.Id != existingByUid.Id {
|
||||
return m.ErrDashboardWithSameUIDExists
|
||||
}
|
||||
|
||||
if dash.Id == 0 {
|
||||
dash.Version = 1
|
||||
metrics.M_Api_Dashboard_Insert.Inc()
|
||||
dash.Data.Set("version", dash.Version)
|
||||
affectedRows, err = sess.Insert(dash)
|
||||
existing := existingById
|
||||
|
||||
if !dashWithIdExists && dashWithUidExists {
|
||||
dash.Id = existingByUid.Id
|
||||
dash.Created = existingByUid.Created
|
||||
dash.CreatedBy = existingByUid.CreatedBy
|
||||
existing = existingByUid
|
||||
}
|
||||
|
||||
if (existing.IsFolder && !cmd.IsFolder) ||
|
||||
(!existing.IsFolder && cmd.IsFolder) {
|
||||
return m.ErrDashboardTypeMismatch
|
||||
}
|
||||
|
||||
// check for is someone else has written in between
|
||||
if dash.Version != existing.Version {
|
||||
if cmd.Overwrite {
|
||||
dash.Version = existing.Version
|
||||
} else {
|
||||
dash.Version++
|
||||
dash.Data.Set("version", dash.Version)
|
||||
|
||||
if !cmd.UpdatedAt.IsZero() {
|
||||
dash.Updated = cmd.UpdatedAt
|
||||
}
|
||||
|
||||
affectedRows, err = sess.MustCols("folder_id", "has_acl").ID(dash.Id).Update(dash)
|
||||
return m.ErrDashboardVersionMismatch
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// do not allow plugin dashboard updates without overwrite flag
|
||||
if existing.PluginId != "" && cmd.Overwrite == false {
|
||||
return m.UpdatePluginDashboardError{PluginId: existing.PluginId}
|
||||
}
|
||||
|
||||
if affectedRows == 0 {
|
||||
return m.ErrDashboardNotFound
|
||||
}
|
||||
|
||||
dashVersion := &m.DashboardVersion{
|
||||
DashboardId: dash.Id,
|
||||
ParentVersion: parentVersion,
|
||||
RestoredFrom: cmd.RestoredFrom,
|
||||
Version: dash.Version,
|
||||
Created: time.Now(),
|
||||
CreatedBy: dash.UpdatedBy,
|
||||
Message: cmd.Message,
|
||||
Data: dash.Data,
|
||||
}
|
||||
|
||||
// insert version entry
|
||||
if affectedRows, err = sess.Insert(dashVersion); err != nil {
|
||||
return err
|
||||
} else if affectedRows == 0 {
|
||||
return m.ErrDashboardNotFound
|
||||
}
|
||||
|
||||
// delete existing tags
|
||||
_, err = sess.Exec("DELETE FROM dashboard_tag WHERE dashboard_id=?", dash.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// insert new tags
|
||||
tags := dash.GetTags()
|
||||
if len(tags) > 0 {
|
||||
for _, tag := range tags {
|
||||
if _, err := sess.Insert(&DashboardTag{DashboardId: dash.Id, Term: tag}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cmd.Result = dash
|
||||
|
||||
return err
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateNewDashboardUid(sess *DBSession, orgId int64) (string, error) {
|
||||
@ -186,23 +241,6 @@ func generateNewDashboardUid(sess *DBSession, orgId int64) (string, error) {
|
||||
return "", m.ErrDashboardFailedGenerateUniqueUid
|
||||
}
|
||||
|
||||
func guaranteeDashboardNameIsUniqueInFolder(sess *DBSession, dash *m.Dashboard) error {
|
||||
var sameNameInFolder m.Dashboard
|
||||
sameNameInFolderExist, err := sess.Where("org_id=? AND title=? AND folder_id = ? AND uid <> ?",
|
||||
dash.OrgId, dash.Title, dash.FolderId, dash.Uid).
|
||||
Get(&sameNameInFolder)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if sameNameInFolderExist {
|
||||
return m.ErrDashboardWithSameNameInFolderExists
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setHasAcl(sess *DBSession, dash *m.Dashboard) error {
|
||||
// check if parent has acl
|
||||
if dash.FolderId > 0 {
|
||||
@ -252,6 +290,7 @@ type DashboardSearchProjection struct {
|
||||
Term string
|
||||
IsFolder bool
|
||||
FolderId int64
|
||||
FolderUid string
|
||||
FolderSlug string
|
||||
FolderTitle string
|
||||
}
|
||||
@ -330,11 +369,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
|
||||
}
|
||||
@ -366,8 +409,9 @@ func GetFoldersForSignedInUser(query *m.GetFoldersForSignedInUserQuery) error {
|
||||
|
||||
if query.SignedInUser.OrgRole == m.ROLE_ADMIN {
|
||||
sql := `SELECT distinct d.id, d.title
|
||||
FROM dashboard AS d WHERE d.is_folder = ?`
|
||||
FROM dashboard AS d WHERE d.is_folder = ? AND d.org_id = ?`
|
||||
params = append(params, dialect.BooleanStr(true))
|
||||
params = append(params, query.OrgId)
|
||||
|
||||
if len(query.Title) > 0 {
|
||||
sql += " AND d.title " + dialect.LikeStr() + " ?"
|
||||
@ -432,6 +476,7 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
|
||||
"DELETE FROM dashboard_version WHERE dashboard_id = ?",
|
||||
"DELETE FROM dashboard WHERE folder_id = ?",
|
||||
"DELETE FROM annotation WHERE dashboard_id = ?",
|
||||
"DELETE FROM dashboard_provisioning WHERE dashboard_id = ?",
|
||||
}
|
||||
|
||||
for _, sql := range deletes {
|
||||
@ -527,9 +572,7 @@ func GetDashboardPermissionsForUser(query *m.GetDashboardPermissionsForUserQuery
|
||||
params = append(params, query.UserId)
|
||||
params = append(params, dialect.BooleanStr(false))
|
||||
|
||||
x.ShowSQL(true)
|
||||
err := x.Sql(sql, params...).Find(&query.Result)
|
||||
x.ShowSQL(false)
|
||||
|
||||
for _, p := range query.Result {
|
||||
p.PermissionName = p.Permission.String()
|
||||
@ -583,7 +626,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
|
||||
|
@ -219,13 +219,14 @@ func TestDashboardFolderDataAccess(t *testing.T) {
|
||||
|
||||
folder1 := insertTestDashboard("1 test dash folder", 1, 0, true, "prod")
|
||||
folder2 := insertTestDashboard("2 test dash folder", 1, 0, true, "prod")
|
||||
insertTestDashboard("folder in another org", 2, 0, true, "prod")
|
||||
|
||||
adminUser := createUser("admin", "Admin", true)
|
||||
editorUser := createUser("editor", "Editor", false)
|
||||
viewerUser := createUser("viewer", "Viewer", false)
|
||||
|
||||
Convey("Admin users", func() {
|
||||
Convey("Should have write access to all dashboard folders", func() {
|
||||
Convey("Should have write access to all dashboard folders in their org", func() {
|
||||
query := m.GetFoldersForSignedInUserQuery{
|
||||
OrgId: 1,
|
||||
SignedInUser: &m.SignedInUser{UserId: adminUser.Id, OrgRole: m.ROLE_ADMIN},
|
||||
|
66
pkg/services/sqlstore/dashboard_provisioning.go
Normal file
66
pkg/services/sqlstore/dashboard_provisioning.go
Normal file
@ -0,0 +1,66 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
func init() {
|
||||
bus.AddHandler("sql", GetProvisionedDashboardDataQuery)
|
||||
bus.AddHandler("sql", SaveProvisionedDashboard)
|
||||
}
|
||||
|
||||
type DashboardExtras struct {
|
||||
Id int64
|
||||
DashboardId int64
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
func SaveProvisionedDashboard(cmd *models.SaveProvisionedDashboardCommand) error {
|
||||
return inTransaction(func(sess *DBSession) error {
|
||||
err := saveDashboard(sess, cmd.DashboardCmd)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.Result = cmd.DashboardCmd.Result
|
||||
if cmd.DashboardProvisioning.Updated.IsZero() {
|
||||
cmd.DashboardProvisioning.Updated = cmd.Result.Updated
|
||||
}
|
||||
|
||||
return saveProvionedData(sess, cmd.DashboardProvisioning, cmd.Result)
|
||||
})
|
||||
}
|
||||
|
||||
func saveProvionedData(sess *DBSession, cmd *models.DashboardProvisioning, dashboard *models.Dashboard) error {
|
||||
result := &models.DashboardProvisioning{}
|
||||
|
||||
exist, err := sess.Where("dashboard_id=?", dashboard.Id).Get(result)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.Id = result.Id
|
||||
cmd.DashboardId = dashboard.Id
|
||||
|
||||
if exist {
|
||||
_, err = sess.ID(result.Id).Update(cmd)
|
||||
} else {
|
||||
_, err = sess.Insert(cmd)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func GetProvisionedDashboardDataQuery(cmd *models.GetProvisionedDashboardDataQuery) error {
|
||||
var result []*models.DashboardProvisioning
|
||||
|
||||
if err := x.Where("name = ?", cmd.Name).Find(&result); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.Result = result
|
||||
return nil
|
||||
}
|
50
pkg/services/sqlstore/dashboard_provisioning_test.go
Normal file
50
pkg/services/sqlstore/dashboard_provisioning_test.go
Normal file
@ -0,0 +1,50 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestDashboardProvisioningTest(t *testing.T) {
|
||||
Convey("Testing Dashboard provisioning", t, func() {
|
||||
InitTestDB(t)
|
||||
|
||||
saveDashboardCmd := &models.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
FolderId: 0,
|
||||
IsFolder: false,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": nil,
|
||||
"title": "test dashboard",
|
||||
}),
|
||||
}
|
||||
|
||||
Convey("Saving dashboards with extras", func() {
|
||||
cmd := &models.SaveProvisionedDashboardCommand{
|
||||
DashboardCmd: saveDashboardCmd,
|
||||
DashboardProvisioning: &models.DashboardProvisioning{
|
||||
Name: "default",
|
||||
ExternalId: "/var/grafana.json",
|
||||
},
|
||||
}
|
||||
|
||||
err := SaveProvisionedDashboard(cmd)
|
||||
So(err, ShouldBeNil)
|
||||
So(cmd.Result, ShouldNotBeNil)
|
||||
So(cmd.Result.Id, ShouldNotEqual, 0)
|
||||
dashId := cmd.Result.Id
|
||||
|
||||
Convey("Can query for provisioned dashboards", func() {
|
||||
query := &models.GetProvisionedDashboardDataQuery{Name: "default"}
|
||||
err := GetProvisionedDashboardDataQuery(query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(len(query.Result), ShouldEqual, 1)
|
||||
So(query.Result[0].DashboardId, ShouldEqual, dashId)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
@ -100,7 +100,7 @@ func TestDashboardDataAccess(t *testing.T) {
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Should return error if no dashboard is updated", func() {
|
||||
Convey("Should return not found error if no dashboard is found for update", func() {
|
||||
cmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Overwrite: true,
|
||||
@ -112,7 +112,7 @@ func TestDashboardDataAccess(t *testing.T) {
|
||||
}
|
||||
|
||||
err := SaveDashboard(&cmd)
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err, ShouldEqual, m.ErrDashboardNotFound)
|
||||
})
|
||||
|
||||
Convey("Should not be able to overwrite dashboard in another org", func() {
|
||||
@ -130,70 +130,7 @@ func TestDashboardDataAccess(t *testing.T) {
|
||||
}
|
||||
|
||||
err := SaveDashboard(&cmd)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("Should be able to search for dashboard folder", func() {
|
||||
query := search.FindPersistedDashboardsQuery{
|
||||
Title: "1 test dash folder",
|
||||
OrgId: 1,
|
||||
SignedInUser: &m.SignedInUser{OrgId: 1},
|
||||
}
|
||||
|
||||
err := SearchDashboards(&query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(len(query.Result), ShouldEqual, 1)
|
||||
hit := query.Result[0]
|
||||
So(hit.Type, ShouldEqual, search.DashHitFolder)
|
||||
So(hit.Url, ShouldEqual, fmt.Sprintf("/dashboards/f/%s/%s", savedFolder.Uid, savedFolder.Slug))
|
||||
})
|
||||
|
||||
Convey("Should be able to search for a dashboard folder's children", func() {
|
||||
query := search.FindPersistedDashboardsQuery{
|
||||
OrgId: 1,
|
||||
FolderIds: []int64{savedFolder.Id},
|
||||
SignedInUser: &m.SignedInUser{OrgId: 1},
|
||||
}
|
||||
|
||||
err := SearchDashboards(&query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(len(query.Result), ShouldEqual, 2)
|
||||
hit := query.Result[0]
|
||||
So(hit.Id, ShouldEqual, savedDash.Id)
|
||||
So(hit.Url, ShouldEqual, fmt.Sprintf("/d/%s/%s", savedDash.Uid, savedDash.Slug))
|
||||
})
|
||||
|
||||
Convey("Should be able to search for dashboard by dashboard ids", func() {
|
||||
Convey("should be able to find two dashboards by id", func() {
|
||||
query := search.FindPersistedDashboardsQuery{
|
||||
DashboardIds: []int64{2, 3},
|
||||
SignedInUser: &m.SignedInUser{OrgId: 1},
|
||||
}
|
||||
|
||||
err := SearchDashboards(&query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(len(query.Result), ShouldEqual, 2)
|
||||
|
||||
hit := query.Result[0]
|
||||
So(len(hit.Tags), ShouldEqual, 2)
|
||||
|
||||
hit2 := query.Result[1]
|
||||
So(len(hit2.Tags), ShouldEqual, 1)
|
||||
})
|
||||
|
||||
Convey("DashboardIds that does not exists should not cause errors", func() {
|
||||
query := search.FindPersistedDashboardsQuery{
|
||||
DashboardIds: []int64{1000},
|
||||
SignedInUser: &m.SignedInUser{OrgId: 1},
|
||||
}
|
||||
|
||||
err := SearchDashboards(&query)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result), ShouldEqual, 0)
|
||||
})
|
||||
So(err, ShouldEqual, m.ErrDashboardNotFound)
|
||||
})
|
||||
|
||||
Convey("Should be able to save dashboards with same name in different folders", func() {
|
||||
@ -224,9 +161,140 @@ func TestDashboardDataAccess(t *testing.T) {
|
||||
|
||||
err = SaveDashboard(&secondSaveCmd)
|
||||
So(err, ShouldBeNil)
|
||||
So(firstSaveCmd.Result.Id, ShouldNotEqual, secondSaveCmd.Result.Id)
|
||||
})
|
||||
|
||||
Convey("Should not be able to save dashboard with same name in the same folder", func() {
|
||||
Convey("Should be able to overwrite dashboard in same folder using title", func() {
|
||||
insertTestDashboard("Dash", 1, 0, false, "prod", "webapp")
|
||||
folder := insertTestDashboard("Folder", 1, 0, true, "prod", "webapp")
|
||||
dashInFolder := insertTestDashboard("Dash", 1, folder.Id, false, "prod", "webapp")
|
||||
|
||||
cmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"title": "Dash",
|
||||
}),
|
||||
FolderId: folder.Id,
|
||||
Overwrite: true,
|
||||
}
|
||||
|
||||
err := SaveDashboard(&cmd)
|
||||
So(err, ShouldBeNil)
|
||||
So(cmd.Result.Id, ShouldEqual, dashInFolder.Id)
|
||||
So(cmd.Result.Uid, ShouldEqual, dashInFolder.Uid)
|
||||
})
|
||||
|
||||
Convey("Should be able to overwrite dashboard in General folder using title", func() {
|
||||
dashInGeneral := insertTestDashboard("Dash", 1, 0, false, "prod", "webapp")
|
||||
folder := insertTestDashboard("Folder", 1, 0, true, "prod", "webapp")
|
||||
insertTestDashboard("Dash", 1, folder.Id, false, "prod", "webapp")
|
||||
|
||||
cmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"title": "Dash",
|
||||
}),
|
||||
FolderId: 0,
|
||||
Overwrite: true,
|
||||
}
|
||||
|
||||
err := SaveDashboard(&cmd)
|
||||
So(err, ShouldBeNil)
|
||||
So(cmd.Result.Id, ShouldEqual, dashInGeneral.Id)
|
||||
So(cmd.Result.Uid, ShouldEqual, dashInGeneral.Uid)
|
||||
})
|
||||
|
||||
Convey("Should not be able to overwrite folder with dashboard in general folder using title", func() {
|
||||
cmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"title": savedFolder.Title,
|
||||
}),
|
||||
FolderId: 0,
|
||||
IsFolder: false,
|
||||
Overwrite: true,
|
||||
}
|
||||
|
||||
err := SaveDashboard(&cmd)
|
||||
So(err, ShouldEqual, m.ErrDashboardWithSameNameAsFolder)
|
||||
})
|
||||
|
||||
Convey("Should not be able to overwrite folder with dashboard in folder using title", func() {
|
||||
cmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"title": savedFolder.Title,
|
||||
}),
|
||||
FolderId: savedFolder.Id,
|
||||
IsFolder: false,
|
||||
Overwrite: true,
|
||||
}
|
||||
|
||||
err := SaveDashboard(&cmd)
|
||||
So(err, ShouldEqual, m.ErrDashboardWithSameNameAsFolder)
|
||||
})
|
||||
|
||||
Convey("Should not be able to overwrite folder with dashboard using id", func() {
|
||||
cmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": savedFolder.Id,
|
||||
"title": "new title",
|
||||
}),
|
||||
IsFolder: false,
|
||||
Overwrite: true,
|
||||
}
|
||||
|
||||
err := SaveDashboard(&cmd)
|
||||
So(err, ShouldEqual, m.ErrDashboardTypeMismatch)
|
||||
})
|
||||
|
||||
Convey("Should not be able to overwrite dashboard with folder using id", func() {
|
||||
cmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": savedDash.Id,
|
||||
"title": "new folder title",
|
||||
}),
|
||||
IsFolder: true,
|
||||
Overwrite: true,
|
||||
}
|
||||
|
||||
err := SaveDashboard(&cmd)
|
||||
So(err, ShouldEqual, m.ErrDashboardTypeMismatch)
|
||||
})
|
||||
|
||||
Convey("Should not be able to overwrite folder with dashboard using uid", func() {
|
||||
cmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"uid": savedFolder.Uid,
|
||||
"title": "new title",
|
||||
}),
|
||||
IsFolder: false,
|
||||
Overwrite: true,
|
||||
}
|
||||
|
||||
err := SaveDashboard(&cmd)
|
||||
So(err, ShouldEqual, m.ErrDashboardTypeMismatch)
|
||||
})
|
||||
|
||||
Convey("Should not be able to overwrite dashboard with folder using uid", func() {
|
||||
cmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"uid": savedDash.Uid,
|
||||
"title": "new folder title",
|
||||
}),
|
||||
IsFolder: true,
|
||||
Overwrite: true,
|
||||
}
|
||||
|
||||
err := SaveDashboard(&cmd)
|
||||
So(err, ShouldEqual, m.ErrDashboardTypeMismatch)
|
||||
})
|
||||
|
||||
Convey("Should not be able to save dashboard with same name in the same folder without overwrite", func() {
|
||||
firstSaveCmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
@ -256,20 +324,49 @@ func TestDashboardDataAccess(t *testing.T) {
|
||||
So(err, ShouldEqual, m.ErrDashboardWithSameNameInFolderExists)
|
||||
})
|
||||
|
||||
Convey("Should not be able to save dashboard with same uid", func() {
|
||||
Convey("Should be able to save and update dashboard using same uid", func() {
|
||||
cmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": nil,
|
||||
"title": "test dash 23",
|
||||
"uid": "dsfalkjngailuedt",
|
||||
"title": "test dash 23",
|
||||
}),
|
||||
}
|
||||
|
||||
err := SaveDashboard(&cmd)
|
||||
So(err, ShouldBeNil)
|
||||
err = SaveDashboard(&cmd)
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Should be able to update dashboard using uid", func() {
|
||||
cmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"uid": savedDash.Uid,
|
||||
"title": "new title",
|
||||
}),
|
||||
FolderId: 0,
|
||||
Overwrite: true,
|
||||
}
|
||||
|
||||
err := SaveDashboard(&cmd)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("Should be able to get updated dashboard by uid", func() {
|
||||
query := m.GetDashboardQuery{
|
||||
Uid: savedDash.Uid,
|
||||
OrgId: 1,
|
||||
}
|
||||
|
||||
err := GetDashboard(&query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(query.Result.Id, ShouldEqual, savedDash.Id)
|
||||
So(query.Result.Title, ShouldEqual, "new title")
|
||||
So(query.Result.FolderId, ShouldEqual, 0)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Should be able to update dashboard with the same title and folder id", func() {
|
||||
@ -305,7 +402,7 @@ func TestDashboardDataAccess(t *testing.T) {
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Should not be able to update using just uid", func() {
|
||||
Convey("Should be able to update using uid without id and overwrite", func() {
|
||||
cmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
@ -317,23 +414,6 @@ func TestDashboardDataAccess(t *testing.T) {
|
||||
FolderId: savedDash.FolderId,
|
||||
}
|
||||
|
||||
err := SaveDashboard(&cmd)
|
||||
So(err, ShouldEqual, m.ErrDashboardWithSameUIDExists)
|
||||
})
|
||||
|
||||
Convey("Should be able to update using just uid with overwrite", func() {
|
||||
cmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"uid": savedDash.Uid,
|
||||
"title": "folderId",
|
||||
"version": savedDash.Version,
|
||||
"tags": []interface{}{},
|
||||
}),
|
||||
FolderId: savedDash.FolderId,
|
||||
Overwrite: true,
|
||||
}
|
||||
|
||||
err := SaveDashboard(&cmd)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
@ -362,11 +442,11 @@ func TestDashboardDataAccess(t *testing.T) {
|
||||
generateNewUid = util.GenerateShortUid
|
||||
})
|
||||
|
||||
Convey("Should be able to update dashboard and remove folderId", func() {
|
||||
Convey("Should be able to update dashboard by id and remove folderId", func() {
|
||||
cmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": 1,
|
||||
"id": savedDash.Id,
|
||||
"title": "folderId",
|
||||
"tags": []interface{}{},
|
||||
}),
|
||||
@ -381,7 +461,7 @@ func TestDashboardDataAccess(t *testing.T) {
|
||||
cmd = m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": 1,
|
||||
"id": savedDash.Id,
|
||||
"title": "folderId",
|
||||
"tags": []interface{}{},
|
||||
}),
|
||||
@ -393,7 +473,7 @@ func TestDashboardDataAccess(t *testing.T) {
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
query := m.GetDashboardQuery{
|
||||
Slug: cmd.Result.Slug,
|
||||
Id: savedDash.Id,
|
||||
OrgId: 1,
|
||||
}
|
||||
|
||||
@ -428,6 +508,63 @@ func TestDashboardDataAccess(t *testing.T) {
|
||||
So(len(query.Result), ShouldEqual, 2)
|
||||
})
|
||||
|
||||
Convey("Should be able to search for dashboard folder", func() {
|
||||
query := search.FindPersistedDashboardsQuery{
|
||||
Title: "1 test dash folder",
|
||||
OrgId: 1,
|
||||
SignedInUser: &m.SignedInUser{OrgId: 1},
|
||||
}
|
||||
|
||||
err := SearchDashboards(&query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(len(query.Result), ShouldEqual, 1)
|
||||
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() {
|
||||
query := search.FindPersistedDashboardsQuery{
|
||||
OrgId: 1,
|
||||
FolderIds: []int64{savedFolder.Id},
|
||||
SignedInUser: &m.SignedInUser{OrgId: 1},
|
||||
}
|
||||
|
||||
err := SearchDashboards(&query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(len(query.Result), ShouldEqual, 2)
|
||||
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() {
|
||||
Convey("should be able to find two dashboards by id", func() {
|
||||
query := search.FindPersistedDashboardsQuery{
|
||||
DashboardIds: []int64{2, 3},
|
||||
SignedInUser: &m.SignedInUser{OrgId: 1},
|
||||
}
|
||||
|
||||
err := SearchDashboards(&query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(len(query.Result), ShouldEqual, 2)
|
||||
|
||||
hit := query.Result[0]
|
||||
So(len(hit.Tags), ShouldEqual, 2)
|
||||
|
||||
hit2 := query.Result[1]
|
||||
So(len(hit2.Tags), ShouldEqual, 1)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given two dashboards, one is starred dashboard by user 10, other starred by user 1", func() {
|
||||
starredDash := insertTestDashboard("starred dash", 1, 0, false)
|
||||
StarDashboard(&m.StarDashboardCommand{
|
||||
|
@ -1,6 +1,8 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/go-xorm/xorm"
|
||||
@ -11,10 +13,33 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
|
||||
)
|
||||
|
||||
var (
|
||||
dbSqlite = "sqlite"
|
||||
dbMySql = "mysql"
|
||||
dbPostgres = "postgres"
|
||||
)
|
||||
|
||||
func InitTestDB(t *testing.T) *xorm.Engine {
|
||||
x, err := xorm.NewEngine(sqlutil.TestDB_Sqlite3.DriverName, sqlutil.TestDB_Sqlite3.ConnStr)
|
||||
//x, err := xorm.NewEngine(sqlutil.TestDB_Mysql.DriverName, sqlutil.TestDB_Mysql.ConnStr)
|
||||
//x, err := xorm.NewEngine(sqlutil.TestDB_Postgres.DriverName, sqlutil.TestDB_Postgres.ConnStr)
|
||||
selectedDb := dbSqlite
|
||||
//selectedDb := dbMySql
|
||||
//selectedDb := dbPostgres
|
||||
|
||||
var x *xorm.Engine
|
||||
var err error
|
||||
|
||||
// environment variable present for test db?
|
||||
if db, present := os.LookupEnv("GRAFANA_TEST_DB"); present {
|
||||
selectedDb = db
|
||||
}
|
||||
|
||||
switch strings.ToLower(selectedDb) {
|
||||
case dbMySql:
|
||||
x, err = xorm.NewEngine(sqlutil.TestDB_Mysql.DriverName, sqlutil.TestDB_Mysql.ConnStr)
|
||||
case dbPostgres:
|
||||
x, err = xorm.NewEngine(sqlutil.TestDB_Postgres.DriverName, sqlutil.TestDB_Postgres.ConnStr)
|
||||
default:
|
||||
x, err = xorm.NewEngine(sqlutil.TestDB_Sqlite3.DriverName, sqlutil.TestDB_Sqlite3.ConnStr)
|
||||
}
|
||||
|
||||
// x.ShowSQL()
|
||||
|
||||
|
@ -167,4 +167,29 @@ 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,
|
||||
}))
|
||||
|
||||
dashboardExtrasTable := Table{
|
||||
Name: "dashboard_provisioning",
|
||||
Columns: []*Column{
|
||||
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||
{Name: "dashboard_id", Type: DB_BigInt, Nullable: true},
|
||||
{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "external_id", Type: DB_Text, Nullable: false},
|
||||
{Name: "updated", Type: DB_DateTime, Nullable: false},
|
||||
},
|
||||
Indices: []*Index{
|
||||
{Cols: []string{"dashboard_id"}},
|
||||
{Cols: []string{"dashboard_id", "name"}, Type: IndexType},
|
||||
},
|
||||
}
|
||||
|
||||
mg.AddMigration("create dashboard_provisioning", NewAddTableMigration(dashboardExtrasTable))
|
||||
}
|
||||
|
@ -123,6 +123,31 @@ func TestAccountDataAccess(t *testing.T) {
|
||||
So(query.Result[0].Role, ShouldEqual, "Admin")
|
||||
})
|
||||
|
||||
Convey("Can get organization users with query", func() {
|
||||
query := m.GetOrgUsersQuery{
|
||||
OrgId: ac1.OrgId,
|
||||
Query: "ac1",
|
||||
}
|
||||
err := GetOrgUsers(&query)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result), ShouldEqual, 1)
|
||||
So(query.Result[0].Email, ShouldEqual, ac1.Email)
|
||||
})
|
||||
|
||||
Convey("Can get organization users with query and limit", func() {
|
||||
query := m.GetOrgUsersQuery{
|
||||
OrgId: ac1.OrgId,
|
||||
Query: "ac",
|
||||
Limit: 1,
|
||||
}
|
||||
err := GetOrgUsers(&query)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result), ShouldEqual, 1)
|
||||
So(query.Result[0].Email, ShouldEqual, ac1.Email)
|
||||
})
|
||||
|
||||
Convey("Can set using org", func() {
|
||||
cmd := m.SetUsingOrgCommand{UserId: ac2.Id, OrgId: ac1.Id}
|
||||
err := SetUsingOrg(&cmd)
|
||||
|
@ -2,6 +2,7 @@ package sqlstore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
@ -69,9 +70,30 @@ func UpdateOrgUser(cmd *m.UpdateOrgUserCommand) error {
|
||||
|
||||
func GetOrgUsers(query *m.GetOrgUsersQuery) error {
|
||||
query.Result = make([]*m.OrgUserDTO, 0)
|
||||
|
||||
sess := x.Table("org_user")
|
||||
sess.Join("INNER", "user", fmt.Sprintf("org_user.user_id=%s.id", x.Dialect().Quote("user")))
|
||||
sess.Where("org_user.org_id=?", query.OrgId)
|
||||
|
||||
whereConditions := make([]string, 0)
|
||||
whereParams := make([]interface{}, 0)
|
||||
|
||||
whereConditions = append(whereConditions, "org_user.org_id = ?")
|
||||
whereParams = append(whereParams, query.OrgId)
|
||||
|
||||
if query.Query != "" {
|
||||
queryWithWildcards := "%" + query.Query + "%"
|
||||
whereConditions = append(whereConditions, "(email "+dialect.LikeStr()+" ? OR name "+dialect.LikeStr()+" ? OR login "+dialect.LikeStr()+" ?)")
|
||||
whereParams = append(whereParams, queryWithWildcards, queryWithWildcards, queryWithWildcards)
|
||||
}
|
||||
|
||||
if len(whereConditions) > 0 {
|
||||
sess.Where(strings.Join(whereConditions, " AND "), whereParams...)
|
||||
}
|
||||
|
||||
if query.Limit > 0 {
|
||||
sess.Limit(query.Limit, 0)
|
||||
}
|
||||
|
||||
sess.Cols("org_user.org_id", "org_user.user_id", "user.email", "user.login", "org_user.role", "user.last_seen_at")
|
||||
sess.Asc("user.email", "user.login")
|
||||
|
||||
|
@ -1,8 +1,6 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
@ -25,8 +23,6 @@ func CreatePlaylist(cmd *m.CreatePlaylistCommand) error {
|
||||
|
||||
_, err := x.Insert(&playlist)
|
||||
|
||||
fmt.Printf("%v", playlist.Id)
|
||||
|
||||
playlistItems := make([]m.PlaylistItem, 0)
|
||||
for _, item := range cmd.Items {
|
||||
playlistItems = append(playlistItems, m.PlaylistItem{
|
||||
|
@ -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 `)
|
||||
|
@ -25,7 +25,7 @@ func init() {
|
||||
func CreateTeam(cmd *m.CreateTeamCommand) error {
|
||||
return inTransaction(func(sess *DBSession) error {
|
||||
|
||||
if isNameTaken, err := isTeamNameTaken(cmd.Name, 0, sess); err != nil {
|
||||
if isNameTaken, err := isTeamNameTaken(cmd.OrgId, cmd.Name, 0, sess); err != nil {
|
||||
return err
|
||||
} else if isNameTaken {
|
||||
return m.ErrTeamNameTaken
|
||||
@ -50,7 +50,7 @@ func CreateTeam(cmd *m.CreateTeamCommand) error {
|
||||
func UpdateTeam(cmd *m.UpdateTeamCommand) error {
|
||||
return inTransaction(func(sess *DBSession) error {
|
||||
|
||||
if isNameTaken, err := isTeamNameTaken(cmd.Name, cmd.Id, sess); err != nil {
|
||||
if isNameTaken, err := isTeamNameTaken(cmd.OrgId, cmd.Name, cmd.Id, sess); err != nil {
|
||||
return err
|
||||
} else if isNameTaken {
|
||||
return m.ErrTeamNameTaken
|
||||
@ -80,20 +80,20 @@ func UpdateTeam(cmd *m.UpdateTeamCommand) error {
|
||||
|
||||
func DeleteTeam(cmd *m.DeleteTeamCommand) error {
|
||||
return inTransaction(func(sess *DBSession) error {
|
||||
if res, err := sess.Query("SELECT 1 from team WHERE id=?", cmd.Id); err != nil {
|
||||
if res, err := sess.Query("SELECT 1 from team WHERE org_id=? and id=?", cmd.OrgId, cmd.Id); err != nil {
|
||||
return err
|
||||
} else if len(res) != 1 {
|
||||
return m.ErrTeamNotFound
|
||||
}
|
||||
|
||||
deletes := []string{
|
||||
"DELETE FROM team_member WHERE team_id = ?",
|
||||
"DELETE FROM team WHERE id = ?",
|
||||
"DELETE FROM dashboard_acl WHERE team_id = ?",
|
||||
"DELETE FROM team_member WHERE org_id=? and team_id = ?",
|
||||
"DELETE FROM team WHERE org_id=? and id = ?",
|
||||
"DELETE FROM dashboard_acl WHERE org_id=? and team_id = ?",
|
||||
}
|
||||
|
||||
for _, sql := range deletes {
|
||||
_, err := sess.Exec(sql, cmd.Id)
|
||||
_, err := sess.Exec(sql, cmd.OrgId, cmd.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -102,9 +102,9 @@ func DeleteTeam(cmd *m.DeleteTeamCommand) error {
|
||||
})
|
||||
}
|
||||
|
||||
func isTeamNameTaken(name string, existingId int64, sess *DBSession) (bool, error) {
|
||||
func isTeamNameTaken(orgId int64, name string, existingId int64, sess *DBSession) (bool, error) {
|
||||
var team m.Team
|
||||
exists, err := sess.Where("name=?", name).Get(&team)
|
||||
exists, err := sess.Where("org_id=? and name=?", orgId, name).Get(&team)
|
||||
|
||||
if err != nil {
|
||||
return false, nil
|
||||
@ -128,6 +128,7 @@ func SearchTeams(query *m.SearchTeamsQuery) error {
|
||||
|
||||
sql.WriteString(`select
|
||||
team.id as id,
|
||||
team.org_id,
|
||||
team.name as name,
|
||||
team.email as email,
|
||||
(select count(*) from team_member where team_member.team_id = team.id) as member_count
|
||||
@ -176,7 +177,7 @@ func SearchTeams(query *m.SearchTeamsQuery) error {
|
||||
|
||||
func GetTeamById(query *m.GetTeamByIdQuery) error {
|
||||
var team m.Team
|
||||
exists, err := x.Id(query.Id).Get(&team)
|
||||
exists, err := x.Where("org_id=? and id=?", query.OrgId, query.Id).Get(&team)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -194,7 +195,7 @@ func GetTeamsByUser(query *m.GetTeamsByUserQuery) error {
|
||||
|
||||
sess := x.Table("team")
|
||||
sess.Join("INNER", "team_member", "team.id=team_member.team_id")
|
||||
sess.Where("team_member.user_id=?", query.UserId)
|
||||
sess.Where("team.org_id=? and team_member.user_id=?", query.OrgId, query.UserId)
|
||||
|
||||
err := sess.Find(&query.Result)
|
||||
if err != nil {
|
||||
@ -206,13 +207,13 @@ func GetTeamsByUser(query *m.GetTeamsByUserQuery) error {
|
||||
|
||||
func AddTeamMember(cmd *m.AddTeamMemberCommand) error {
|
||||
return inTransaction(func(sess *DBSession) error {
|
||||
if res, err := sess.Query("SELECT 1 from team_member WHERE team_id=? and user_id=?", cmd.TeamId, cmd.UserId); err != nil {
|
||||
if res, err := sess.Query("SELECT 1 from team_member WHERE org_id=? and team_id=? and user_id=?", cmd.OrgId, cmd.TeamId, cmd.UserId); err != nil {
|
||||
return err
|
||||
} else if len(res) == 1 {
|
||||
return m.ErrTeamMemberAlreadyAdded
|
||||
}
|
||||
|
||||
if res, err := sess.Query("SELECT 1 from team WHERE id=?", cmd.TeamId); err != nil {
|
||||
if res, err := sess.Query("SELECT 1 from team WHERE org_id=? and id=?", cmd.OrgId, cmd.TeamId); err != nil {
|
||||
return err
|
||||
} else if len(res) != 1 {
|
||||
return m.ErrTeamNotFound
|
||||
@ -233,8 +234,8 @@ func AddTeamMember(cmd *m.AddTeamMemberCommand) error {
|
||||
|
||||
func RemoveTeamMember(cmd *m.RemoveTeamMemberCommand) error {
|
||||
return inTransaction(func(sess *DBSession) error {
|
||||
var rawSql = "DELETE FROM team_member WHERE team_id=? and user_id=?"
|
||||
_, err := sess.Exec(rawSql, cmd.TeamId, cmd.UserId)
|
||||
var rawSql = "DELETE FROM team_member WHERE org_id=? and team_id=? and user_id=?"
|
||||
_, err := sess.Exec(rawSql, cmd.OrgId, cmd.TeamId, cmd.UserId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -247,7 +248,7 @@ func GetTeamMembers(query *m.GetTeamMembersQuery) error {
|
||||
query.Result = make([]*m.TeamMemberDTO, 0)
|
||||
sess := x.Table("team_member")
|
||||
sess.Join("INNER", "user", fmt.Sprintf("team_member.user_id=%s.id", x.Dialect().Quote("user")))
|
||||
sess.Where("team_member.team_id=?", query.TeamId)
|
||||
sess.Where("team_member.org_id=? and team_member.team_id=?", query.OrgId, query.TeamId)
|
||||
sess.Cols("user.org_id", "team_member.team_id", "team_member.user_id", "user.email", "user.login")
|
||||
sess.Asc("user.login", "user.email")
|
||||
|
||||
|
@ -27,8 +27,9 @@ func TestTeamCommandsAndQueries(t *testing.T) {
|
||||
userIds = append(userIds, userCmd.Result.Id)
|
||||
}
|
||||
|
||||
group1 := m.CreateTeamCommand{Name: "group1 name", Email: "test1@test.com"}
|
||||
group2 := m.CreateTeamCommand{Name: "group2 name", Email: "test2@test.com"}
|
||||
var testOrgId int64 = 1
|
||||
group1 := m.CreateTeamCommand{OrgId: testOrgId, Name: "group1 name", Email: "test1@test.com"}
|
||||
group2 := m.CreateTeamCommand{OrgId: testOrgId, Name: "group2 name", Email: "test2@test.com"}
|
||||
|
||||
err := CreateTeam(&group1)
|
||||
So(err, ShouldBeNil)
|
||||
@ -36,7 +37,7 @@ func TestTeamCommandsAndQueries(t *testing.T) {
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("Should be able to create teams and add users", func() {
|
||||
query := &m.SearchTeamsQuery{Name: "group1 name", Page: 1, Limit: 10}
|
||||
query := &m.SearchTeamsQuery{OrgId: testOrgId, Name: "group1 name", Page: 1, Limit: 10}
|
||||
err = SearchTeams(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Page, ShouldEqual, 1)
|
||||
@ -44,25 +45,27 @@ func TestTeamCommandsAndQueries(t *testing.T) {
|
||||
team1 := query.Result.Teams[0]
|
||||
So(team1.Name, ShouldEqual, "group1 name")
|
||||
So(team1.Email, ShouldEqual, "test1@test.com")
|
||||
So(team1.OrgId, ShouldEqual, testOrgId)
|
||||
|
||||
err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: 1, TeamId: team1.Id, UserId: userIds[0]})
|
||||
err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: team1.Id, UserId: userIds[0]})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
q1 := &m.GetTeamMembersQuery{TeamId: team1.Id}
|
||||
q1 := &m.GetTeamMembersQuery{OrgId: testOrgId, TeamId: team1.Id}
|
||||
err = GetTeamMembers(q1)
|
||||
So(err, ShouldBeNil)
|
||||
So(q1.Result[0].TeamId, ShouldEqual, team1.Id)
|
||||
So(q1.Result[0].Login, ShouldEqual, "loginuser0")
|
||||
So(q1.Result[0].OrgId, ShouldEqual, testOrgId)
|
||||
})
|
||||
|
||||
Convey("Should be able to search for teams", func() {
|
||||
query := &m.SearchTeamsQuery{Query: "group", Page: 1}
|
||||
query := &m.SearchTeamsQuery{OrgId: testOrgId, Query: "group", Page: 1}
|
||||
err = SearchTeams(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result.Teams), ShouldEqual, 2)
|
||||
So(query.Result.TotalCount, ShouldEqual, 2)
|
||||
|
||||
query2 := &m.SearchTeamsQuery{Query: ""}
|
||||
query2 := &m.SearchTeamsQuery{OrgId: testOrgId, Query: ""}
|
||||
err = SearchTeams(query2)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query2.Result.Teams), ShouldEqual, 2)
|
||||
@ -70,9 +73,9 @@ func TestTeamCommandsAndQueries(t *testing.T) {
|
||||
|
||||
Convey("Should be able to return all teams a user is member of", func() {
|
||||
groupId := group2.Result.Id
|
||||
err := AddTeamMember(&m.AddTeamMemberCommand{OrgId: 1, TeamId: groupId, UserId: userIds[0]})
|
||||
err := AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: groupId, UserId: userIds[0]})
|
||||
|
||||
query := &m.GetTeamsByUserQuery{UserId: userIds[0]}
|
||||
query := &m.GetTeamsByUserQuery{OrgId: testOrgId, UserId: userIds[0]}
|
||||
err = GetTeamsByUser(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result), ShouldEqual, 1)
|
||||
@ -81,7 +84,7 @@ func TestTeamCommandsAndQueries(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Should be able to remove users from a group", func() {
|
||||
err = RemoveTeamMember(&m.RemoveTeamMemberCommand{TeamId: group1.Result.Id, UserId: userIds[0]})
|
||||
err = RemoveTeamMember(&m.RemoveTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0]})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
q1 := &m.GetTeamMembersQuery{TeamId: group1.Result.Id}
|
||||
@ -92,20 +95,20 @@ func TestTeamCommandsAndQueries(t *testing.T) {
|
||||
|
||||
Convey("Should be able to remove a group with users and permissions", func() {
|
||||
groupId := group2.Result.Id
|
||||
err := AddTeamMember(&m.AddTeamMemberCommand{OrgId: 1, TeamId: groupId, UserId: userIds[1]})
|
||||
err := AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: groupId, UserId: userIds[1]})
|
||||
So(err, ShouldBeNil)
|
||||
err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: 1, TeamId: groupId, UserId: userIds[2]})
|
||||
err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: groupId, UserId: userIds[2]})
|
||||
So(err, ShouldBeNil)
|
||||
err = SetDashboardAcl(&m.SetDashboardAclCommand{DashboardId: 1, OrgId: 1, Permission: m.PERMISSION_EDIT, TeamId: groupId})
|
||||
err = SetDashboardAcl(&m.SetDashboardAclCommand{DashboardId: 1, OrgId: testOrgId, Permission: m.PERMISSION_EDIT, TeamId: groupId})
|
||||
|
||||
err = DeleteTeam(&m.DeleteTeamCommand{Id: groupId})
|
||||
err = DeleteTeam(&m.DeleteTeamCommand{OrgId: testOrgId, Id: groupId})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
query := &m.GetTeamByIdQuery{Id: groupId}
|
||||
query := &m.GetTeamByIdQuery{OrgId: testOrgId, Id: groupId}
|
||||
err = GetTeamById(query)
|
||||
So(err, ShouldEqual, m.ErrTeamNotFound)
|
||||
|
||||
permQuery := &m.GetDashboardAclInfoListQuery{DashboardId: 1, OrgId: 1}
|
||||
permQuery := &m.GetDashboardAclInfoListQuery{DashboardId: 1, OrgId: testOrgId}
|
||||
err = GetDashboardAclInfoList(permQuery)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
|
@ -210,7 +210,7 @@ func (s *SocialGithub) UserInfo(client *http.Client, token *oauth2.Token) (*Basi
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error getting user info: %s", err)
|
||||
}
|
||||
|
||||
data.OrganizationsUrl = s.apiUrl + "/user/orgs"
|
||||
userInfo := &BasicUserInfo{
|
||||
Name: data.Login,
|
||||
Login: data.Login,
|
||||
|
@ -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>
|
||||
|
@ -21,19 +21,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>
|
||||
|
@ -31,7 +31,7 @@ class UserPicker extends Component<IProps, any> {
|
||||
|
||||
this.debouncedSearch = debounce(this.search, 300, {
|
||||
leading: true,
|
||||
trailing: false,
|
||||
trailing: true,
|
||||
});
|
||||
}
|
||||
|
||||
@ -39,10 +39,10 @@ class UserPicker extends Component<IProps, any> {
|
||||
const { toggleLoading, backendSrv } = this.props;
|
||||
|
||||
toggleLoading(true);
|
||||
return backendSrv.get(`/api/users/search?perpage=10&page=1&query=${query}`).then(result => {
|
||||
const users = result.users.map(user => {
|
||||
return backendSrv.get(`/api/org/users?query=${query}&limit=10`).then(result => {
|
||||
const users = result.map(user => {
|
||||
return {
|
||||
id: user.id,
|
||||
id: user.userId,
|
||||
label: `${user.login} - ${user.email}`,
|
||||
avatarUrl: user.avatarUrl,
|
||||
login: user.login,
|
||||
|
@ -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() {
|
||||
|
@ -34,6 +34,7 @@ export class FormDropdownCtrl {
|
||||
lookupText: boolean;
|
||||
placeholder: any;
|
||||
startOpen: any;
|
||||
debounce: number;
|
||||
|
||||
/** @ngInject **/
|
||||
constructor(private $scope, $element, private $sce, private templateSrv, private $q) {
|
||||
@ -72,6 +73,10 @@ export class FormDropdownCtrl {
|
||||
this.source(this.query, this.process.bind(this));
|
||||
};
|
||||
|
||||
if (this.debounce) {
|
||||
typeahead.lookup = _.debounce(typeahead.lookup, 500, { leading: true });
|
||||
}
|
||||
|
||||
this.linkElement.keydown(evt => {
|
||||
// trigger typeahead on down arrow or enter key
|
||||
if (evt.keyCode === 40 || evt.keyCode === 13) {
|
||||
@ -263,6 +268,7 @@ export function formDropdownDirective() {
|
||||
lookupText: '@',
|
||||
placeholder: '@',
|
||||
startOpen: '@',
|
||||
debounce: '@',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -23,11 +23,13 @@ export function queryPartEditorDirective($compile, templateSrv) {
|
||||
scope: {
|
||||
part: '=',
|
||||
handleEvent: '&',
|
||||
debounce: '@',
|
||||
},
|
||||
link: function postLink($scope, elem) {
|
||||
var part = $scope.part;
|
||||
var partDef = part.def;
|
||||
var $paramsContainer = elem.find('.query-part-parameters');
|
||||
var debounceLookup = $scope.debounce;
|
||||
|
||||
$scope.partActions = [];
|
||||
|
||||
@ -128,6 +130,10 @@ export function queryPartEditorDirective($compile, templateSrv) {
|
||||
var items = this.source(this.query, $.proxy(this.process, this));
|
||||
return items ? this.process(items) : items;
|
||||
};
|
||||
|
||||
if (debounceLookup) {
|
||||
typeahead.lookup = _.debounce(typeahead.lookup, 500, { leading: true });
|
||||
}
|
||||
}
|
||||
|
||||
$scope.showActionsMenu = function() {
|
||||
|
@ -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));
|
||||
|
@ -22,6 +22,7 @@ function (_, $, coreModule) {
|
||||
segment: "=",
|
||||
getOptions: "&",
|
||||
onChange: "&",
|
||||
debounce: "@",
|
||||
},
|
||||
link: function($scope, elem) {
|
||||
var $input = $(inputTemplate);
|
||||
@ -30,6 +31,7 @@ function (_, $, coreModule) {
|
||||
var options = null;
|
||||
var cancelBlur = null;
|
||||
var linkMode = true;
|
||||
var debounceLookup = $scope.debounce;
|
||||
|
||||
$input.appendTo(elem);
|
||||
$button.appendTo(elem);
|
||||
@ -135,6 +137,10 @@ function (_, $, coreModule) {
|
||||
return items ? this.process(items) : items;
|
||||
};
|
||||
|
||||
if (debounceLookup) {
|
||||
typeahead.lookup = _.debounce(typeahead.lookup, 500, {leading: true});
|
||||
}
|
||||
|
||||
$button.keydown(function(evt) {
|
||||
// trigger typeahead on down arrow or enter key
|
||||
if (evt.keyCode === 40 || evt.keyCode === 13) {
|
||||
|
@ -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,4 +1,5 @@
|
||||
import appEvents from 'app/core/app_events';
|
||||
import locationUtil from 'app/core/utils/location_util';
|
||||
|
||||
export class CreateFolderCtrl {
|
||||
title = '';
|
||||
@ -19,7 +20,7 @@ export class CreateFolderCtrl {
|
||||
|
||||
return this.backendSrv.createFolder({ title: this.title }).then(result => {
|
||||
appEvents.emit('alert-success', ['Folder Created', 'OK']);
|
||||
this.$location.url(result.url);
|
||||
this.$location.url(locationUtil.stripBaseFromUrl(result.url));
|
||||
});
|
||||
}
|
||||
|
||||
@ -27,7 +28,7 @@ export class CreateFolderCtrl {
|
||||
this.titleTouched = true;
|
||||
|
||||
this.validationSrv
|
||||
.validateNewDashboardOrFolderName(this.title)
|
||||
.validateNewFolderName(this.title)
|
||||
.then(() => {
|
||||
this.hasValidationError = false;
|
||||
})
|
||||
|
@ -18,7 +18,7 @@ export class DashboardImportCtrl {
|
||||
nameValidationError: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private backendSrv, private validationSrv, navModelSrv, private $location, private $scope, $routeParams) {
|
||||
constructor(private backendSrv, private validationSrv, navModelSrv, private $location, $routeParams) {
|
||||
this.navModel = navModelSrv.getNav('create', 'import');
|
||||
|
||||
this.step = 1;
|
||||
@ -93,7 +93,7 @@ export class DashboardImportCtrl {
|
||||
this.nameExists = false;
|
||||
|
||||
this.validationSrv
|
||||
.validateNewDashboardOrFolderName(this.dash.title)
|
||||
.validateNewDashboardName(0, this.dash.title)
|
||||
.then(() => {
|
||||
this.hasNameValidationError = false;
|
||||
})
|
||||
@ -124,8 +124,7 @@ export class DashboardImportCtrl {
|
||||
inputs: inputs,
|
||||
})
|
||||
.then(res => {
|
||||
this.$location.url('dashboard/' + res.importedUri);
|
||||
this.$scope.dismiss();
|
||||
this.$location.url(res.importedUrl);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
@ -19,7 +20,10 @@ export class DashboardSrv {
|
||||
return this.dash;
|
||||
}
|
||||
|
||||
handleSaveDashboardError(clone, err) {
|
||||
handleSaveDashboardError(clone, options, err) {
|
||||
options = options || {};
|
||||
options.overwrite = true;
|
||||
|
||||
if (err.data && err.data.status === 'version-mismatch') {
|
||||
err.isHandled = true;
|
||||
|
||||
@ -30,7 +34,7 @@ export class DashboardSrv {
|
||||
yesText: 'Save & Overwrite',
|
||||
icon: 'fa-warning',
|
||||
onConfirm: () => {
|
||||
this.save(clone, { overwrite: true });
|
||||
this.save(clone, options);
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -40,12 +44,12 @@ export class DashboardSrv {
|
||||
|
||||
this.$rootScope.appEvent('confirm-modal', {
|
||||
title: 'Conflict',
|
||||
text: 'Dashboard with the same name exists.',
|
||||
text: 'A dashboard with the same name in selected folder already exists.',
|
||||
text2: 'Would you still like to save this dashboard?',
|
||||
yesText: 'Save & Overwrite',
|
||||
icon: 'fa-warning',
|
||||
onConfirm: () => {
|
||||
this.save(clone, { overwrite: true });
|
||||
this.save(clone, options);
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -74,7 +78,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);
|
||||
@ -90,7 +94,7 @@ export class DashboardSrv {
|
||||
return this.backendSrv
|
||||
.saveDashboard(clone, options)
|
||||
.then(this.postSave.bind(this, clone))
|
||||
.catch(this.handleSaveDashboardError.bind(this, clone));
|
||||
.catch(this.handleSaveDashboardError.bind(this, clone, options));
|
||||
}
|
||||
|
||||
saveDashboard(options, clone) {
|
||||
|
@ -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} />
|
||||
|
@ -67,7 +67,7 @@ export class FolderPickerCtrl {
|
||||
this.newFolderNameTouched = true;
|
||||
|
||||
this.validationSrv
|
||||
.validateNewDashboardOrFolderName(this.newFolderName)
|
||||
.validateNewFolderName(this.newFolderName)
|
||||
.then(() => {
|
||||
this.hasValidationError = false;
|
||||
})
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -19,10 +19,10 @@ describe('DashboardImportCtrl', function() {
|
||||
};
|
||||
|
||||
validationSrv = {
|
||||
validateNewDashboardOrFolderName: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
validateNewDashboardName: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
};
|
||||
|
||||
ctx.ctrl = new DashboardImportCtrl(backendSrv, validationSrv, navModelSrv, {}, {}, {});
|
||||
ctx.ctrl = new DashboardImportCtrl(backendSrv, validationSrv, navModelSrv, {}, {});
|
||||
});
|
||||
|
||||
describe('when uploading json', function() {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user