Merge branch 'master' into docs_v5.0
This commit is contained in:
+12
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
# 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=BC_YRNpqj5k) of Grafana v5.
|
||||
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)
|
||||
@@ -69,6 +69,17 @@ Dashboard panels and rows are positioned using a gridPos object `{x: 0, y: 0, w:
|
||||
## 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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -12,7 +12,7 @@ weight = -6
|
||||
|
||||
# What's New in Grafana v5.0
|
||||
|
||||
> Out in beta: [Download now!](https://www.youtube.com/watch?v=Izr0IBgoTZQ)
|
||||
> 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.
|
||||
|
||||
@@ -24,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
|
||||
@@ -51,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
|
||||
@@ -95,7 +97,7 @@ 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
|
||||
@@ -117,17 +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, new url structure & API changes
|
||||
|
||||
## 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 consistent URL's for accessing dashboards and sharing them
|
||||
between instances. The new routes and API's for accessing dashboards will use the `uid` instead
|
||||
of the `slug`. We'll keep supporting the old routes for accessing dashboards for backward
|
||||
compatibility, but please note that we'll deprecate the old slug-based routes in the future.
|
||||
This means that changing the title of dashboards will not break any bookmarked links.
|
||||
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.
|
||||
|
||||
|
||||
|
||||
+97
-39
@@ -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>
|
||||
|
||||
+7
-7
@@ -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) {
|
||||
|
||||
+10
-2
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
|
||||
@@ -217,6 +218,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 {
|
||||
@@ -237,8 +242,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 {
|
||||
|
||||
@@ -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 {
|
||||
@@ -36,6 +41,11 @@ func GetDashboardAclList(c *middleware.Context) Response {
|
||||
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)
|
||||
@@ -79,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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+11
-7
@@ -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
|
||||
|
||||
+4
-3
@@ -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) {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
+27
-19
@@ -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 {
|
||||
@@ -95,14 +100,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 +125,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
|
||||
}
|
||||
|
||||
|
||||
@@ -95,7 +95,10 @@ type UpdateOrgUserCommand struct {
|
||||
// QUERIES
|
||||
|
||||
type GetOrgUsersQuery struct {
|
||||
OrgId int64
|
||||
OrgId int64
|
||||
Query string
|
||||
Limit int
|
||||
|
||||
Result []*OrgUserDTO
|
||||
}
|
||||
|
||||
|
||||
+5
-1
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -32,47 +32,36 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
|
||||
return inTransaction(func(sess *DBSession) error {
|
||||
dash := cmd.GetDashboardModel()
|
||||
|
||||
// try get existing dashboard
|
||||
var existing m.Dashboard
|
||||
if err := getExistingDashboardForUpdate(sess, dash, cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
var existingByTitleAndFolder m.Dashboard
|
||||
|
||||
// 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
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// do not allow plugin dashboard updates without overwrite flag
|
||||
if existing.PluginId != "" && cmd.Overwrite == false {
|
||||
return m.UpdatePluginDashboardError{PluginId: existing.PluginId}
|
||||
}
|
||||
} 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 !existingByTitleAndFolder.IsFolder && cmd.IsFolder {
|
||||
return m.ErrDashboardFolderWithSameNameAsDashboard
|
||||
}
|
||||
|
||||
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
|
||||
if cmd.Overwrite {
|
||||
dash.Id = existingByTitleAndFolder.Id
|
||||
dash.Version = existingByTitleAndFolder.Version
|
||||
|
||||
if dash.Uid == "" {
|
||||
dash.Uid = existingByTitleAndFolder.Uid
|
||||
}
|
||||
} else {
|
||||
return m.ErrDashboardWithSameNameInFolderExists
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,11 +75,6 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
|
||||
dash.Data.Set("uid", uid)
|
||||
}
|
||||
|
||||
err := guaranteeDashboardNameIsUniqueInFolder(sess, dash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = setHasAcl(sess, dash)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -162,6 +146,72 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if dash.Uid == "" {
|
||||
dash.Uid = existingById.Uid
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
if !dashWithIdExists && !dashWithUidExists {
|
||||
return nil
|
||||
}
|
||||
|
||||
if dashWithIdExists && dashWithUidExists && existingById.Id != existingByUid.Id {
|
||||
return m.ErrDashboardWithSameUIDExists
|
||||
}
|
||||
|
||||
existing := existingById
|
||||
|
||||
if !dashWithIdExists && dashWithUidExists {
|
||||
dash.Id = existingByUid.Id
|
||||
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 {
|
||||
return m.ErrDashboardVersionMismatch
|
||||
}
|
||||
}
|
||||
|
||||
// do not allow plugin dashboard updates without overwrite flag
|
||||
if existing.PluginId != "" && cmd.Overwrite == false {
|
||||
return m.UpdatePluginDashboardError{PluginId: existing.PluginId}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateNewDashboardUid(sess *DBSession, orgId int64) (string, error) {
|
||||
for i := 0; i < 3; i++ {
|
||||
uid := generateNewUid()
|
||||
@@ -179,23 +229,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 {
|
||||
@@ -363,10 +396,10 @@ 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 = ?
|
||||
ORDER BY d.title ASC`
|
||||
|
||||
err = x.Sql(sql, dialect.BooleanStr(true)).Find(&query.Result)
|
||||
err = x.Sql(sql, dialect.BooleanStr(true), query.OrgId).Find(&query.Result)
|
||||
} else {
|
||||
params := make([]interface{}, 0)
|
||||
sql := `SELECT distinct d.id, d.title
|
||||
@@ -518,9 +551,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()
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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,7 +130,382 @@ func TestDashboardDataAccess(t *testing.T) {
|
||||
}
|
||||
|
||||
err := SaveDashboard(&cmd)
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err, ShouldEqual, m.ErrDashboardNotFound)
|
||||
})
|
||||
|
||||
Convey("Should be able to save dashboards with same name in different folders", func() {
|
||||
firstSaveCmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": nil,
|
||||
"title": "test dash folder and title",
|
||||
"tags": []interface{}{},
|
||||
"uid": "randomHash",
|
||||
}),
|
||||
FolderId: 3,
|
||||
}
|
||||
|
||||
err := SaveDashboard(&firstSaveCmd)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
secondSaveCmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": nil,
|
||||
"title": "test dash folder and title",
|
||||
"tags": []interface{}{},
|
||||
"uid": "moreRandomHash",
|
||||
}),
|
||||
FolderId: 1,
|
||||
}
|
||||
|
||||
err = SaveDashboard(&secondSaveCmd)
|
||||
So(err, ShouldBeNil)
|
||||
So(firstSaveCmd.Result.Id, ShouldNotEqual, secondSaveCmd.Result.Id)
|
||||
})
|
||||
|
||||
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{}{
|
||||
"id": nil,
|
||||
"title": "test dash folder and title",
|
||||
"tags": []interface{}{},
|
||||
"uid": "randomHash",
|
||||
}),
|
||||
FolderId: 3,
|
||||
}
|
||||
|
||||
err := SaveDashboard(&firstSaveCmd)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
secondSaveCmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": nil,
|
||||
"title": "test dash folder and title",
|
||||
"tags": []interface{}{},
|
||||
"uid": "moreRandomHash",
|
||||
}),
|
||||
FolderId: 3,
|
||||
}
|
||||
|
||||
err = SaveDashboard(&secondSaveCmd)
|
||||
So(err, ShouldEqual, m.ErrDashboardWithSameNameInFolderExists)
|
||||
})
|
||||
|
||||
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,
|
||||
"uid": "dsfalkjngailuedt",
|
||||
"title": "test dash 23",
|
||||
}),
|
||||
}
|
||||
|
||||
err := SaveDashboard(&cmd)
|
||||
So(err, ShouldBeNil)
|
||||
err = SaveDashboard(&cmd)
|
||||
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() {
|
||||
cmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"uid": "randomHash",
|
||||
"title": "folderId",
|
||||
"style": "light",
|
||||
"tags": []interface{}{},
|
||||
}),
|
||||
FolderId: 2,
|
||||
}
|
||||
|
||||
err := SaveDashboard(&cmd)
|
||||
So(err, ShouldBeNil)
|
||||
So(cmd.Result.FolderId, ShouldEqual, 2)
|
||||
|
||||
cmd = m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": cmd.Result.Id,
|
||||
"uid": "randomHash",
|
||||
"title": "folderId",
|
||||
"style": "dark",
|
||||
"version": cmd.Result.Version,
|
||||
"tags": []interface{}{},
|
||||
}),
|
||||
FolderId: 2,
|
||||
}
|
||||
|
||||
err = SaveDashboard(&cmd)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Should be able to update using uid without id and 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,
|
||||
}
|
||||
|
||||
err := SaveDashboard(&cmd)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Should retry generation of uid once if it fails.", func() {
|
||||
timesCalled := 0
|
||||
generateNewUid = func() string {
|
||||
timesCalled += 1
|
||||
if timesCalled <= 2 {
|
||||
return savedDash.Uid
|
||||
} else {
|
||||
return util.GenerateShortUid()
|
||||
}
|
||||
}
|
||||
cmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"title": "new dash 12334",
|
||||
"tags": []interface{}{},
|
||||
}),
|
||||
}
|
||||
|
||||
err := SaveDashboard(&cmd)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
generateNewUid = util.GenerateShortUid
|
||||
})
|
||||
|
||||
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": savedDash.Id,
|
||||
"title": "folderId",
|
||||
"tags": []interface{}{},
|
||||
}),
|
||||
Overwrite: true,
|
||||
FolderId: 2,
|
||||
}
|
||||
|
||||
err := SaveDashboard(&cmd)
|
||||
So(err, ShouldBeNil)
|
||||
So(cmd.Result.FolderId, ShouldEqual, 2)
|
||||
|
||||
cmd = m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": savedDash.Id,
|
||||
"title": "folderId",
|
||||
"tags": []interface{}{},
|
||||
}),
|
||||
FolderId: 0,
|
||||
Overwrite: true,
|
||||
}
|
||||
|
||||
err = SaveDashboard(&cmd)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
query := m.GetDashboardQuery{
|
||||
Id: savedDash.Id,
|
||||
OrgId: 1,
|
||||
}
|
||||
|
||||
err = GetDashboard(&query)
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result.FolderId, ShouldEqual, 0)
|
||||
})
|
||||
|
||||
Convey("Should be able to delete a dashboard folder and its children", func() {
|
||||
deleteCmd := &m.DeleteDashboardCommand{Id: savedFolder.Id}
|
||||
err := DeleteDashboard(deleteCmd)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
query := search.FindPersistedDashboardsQuery{
|
||||
OrgId: 1,
|
||||
FolderIds: []int64{savedFolder.Id},
|
||||
SignedInUser: &m.SignedInUser{},
|
||||
}
|
||||
|
||||
err = SearchDashboards(&query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(len(query.Result), ShouldEqual, 0)
|
||||
})
|
||||
|
||||
Convey("Should be able to get dashboard tags", func() {
|
||||
query := m.GetDashboardTagsQuery{OrgId: 1}
|
||||
|
||||
err := GetDashboardTags(&query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(len(query.Result), ShouldEqual, 2)
|
||||
})
|
||||
|
||||
Convey("Should be able to search for dashboard folder", func() {
|
||||
@@ -188,249 +563,6 @@ func TestDashboardDataAccess(t *testing.T) {
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Should be able to save dashboards with same name in different folders", func() {
|
||||
firstSaveCmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": nil,
|
||||
"title": "test dash folder and title",
|
||||
"tags": []interface{}{},
|
||||
"uid": "randomHash",
|
||||
}),
|
||||
FolderId: 3,
|
||||
}
|
||||
|
||||
err := SaveDashboard(&firstSaveCmd)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
secondSaveCmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": nil,
|
||||
"title": "test dash folder and title",
|
||||
"tags": []interface{}{},
|
||||
"uid": "moreRandomHash",
|
||||
}),
|
||||
FolderId: 1,
|
||||
}
|
||||
|
||||
err = SaveDashboard(&secondSaveCmd)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Should not be able to save dashboard with same name in the same folder", func() {
|
||||
firstSaveCmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": nil,
|
||||
"title": "test dash folder and title",
|
||||
"tags": []interface{}{},
|
||||
"uid": "randomHash",
|
||||
}),
|
||||
FolderId: 3,
|
||||
}
|
||||
|
||||
err := SaveDashboard(&firstSaveCmd)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
secondSaveCmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": nil,
|
||||
"title": "test dash folder and title",
|
||||
"tags": []interface{}{},
|
||||
"uid": "moreRandomHash",
|
||||
}),
|
||||
FolderId: 3,
|
||||
}
|
||||
|
||||
err = SaveDashboard(&secondSaveCmd)
|
||||
So(err, ShouldEqual, m.ErrDashboardWithSameNameInFolderExists)
|
||||
})
|
||||
|
||||
Convey("Should not be able to save dashboard with same uid", func() {
|
||||
cmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": nil,
|
||||
"title": "test dash 23",
|
||||
"uid": "dsfalkjngailuedt",
|
||||
}),
|
||||
}
|
||||
|
||||
err := SaveDashboard(&cmd)
|
||||
So(err, ShouldBeNil)
|
||||
err = SaveDashboard(&cmd)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("Should be able to update dashboard with the same title and folder id", func() {
|
||||
cmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"uid": "randomHash",
|
||||
"title": "folderId",
|
||||
"style": "light",
|
||||
"tags": []interface{}{},
|
||||
}),
|
||||
FolderId: 2,
|
||||
}
|
||||
|
||||
err := SaveDashboard(&cmd)
|
||||
So(err, ShouldBeNil)
|
||||
So(cmd.Result.FolderId, ShouldEqual, 2)
|
||||
|
||||
cmd = m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": cmd.Result.Id,
|
||||
"uid": "randomHash",
|
||||
"title": "folderId",
|
||||
"style": "dark",
|
||||
"version": cmd.Result.Version,
|
||||
"tags": []interface{}{},
|
||||
}),
|
||||
FolderId: 2,
|
||||
}
|
||||
|
||||
err = SaveDashboard(&cmd)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Should not be able to update using just uid", 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,
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
Convey("Should retry generation of uid once if it fails.", func() {
|
||||
timesCalled := 0
|
||||
generateNewUid = func() string {
|
||||
timesCalled += 1
|
||||
if timesCalled <= 2 {
|
||||
return savedDash.Uid
|
||||
} else {
|
||||
return util.GenerateShortUid()
|
||||
}
|
||||
}
|
||||
cmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"title": "new dash 12334",
|
||||
"tags": []interface{}{},
|
||||
}),
|
||||
}
|
||||
|
||||
err := SaveDashboard(&cmd)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
generateNewUid = util.GenerateShortUid
|
||||
})
|
||||
|
||||
Convey("Should be able to update dashboard and remove folderId", func() {
|
||||
cmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": 1,
|
||||
"title": "folderId",
|
||||
"tags": []interface{}{},
|
||||
}),
|
||||
Overwrite: true,
|
||||
FolderId: 2,
|
||||
}
|
||||
|
||||
err := SaveDashboard(&cmd)
|
||||
So(err, ShouldBeNil)
|
||||
So(cmd.Result.FolderId, ShouldEqual, 2)
|
||||
|
||||
cmd = m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": 1,
|
||||
"title": "folderId",
|
||||
"tags": []interface{}{},
|
||||
}),
|
||||
FolderId: 0,
|
||||
Overwrite: true,
|
||||
}
|
||||
|
||||
err = SaveDashboard(&cmd)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
query := m.GetDashboardQuery{
|
||||
Slug: cmd.Result.Slug,
|
||||
OrgId: 1,
|
||||
}
|
||||
|
||||
err = GetDashboard(&query)
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result.FolderId, ShouldEqual, 0)
|
||||
})
|
||||
|
||||
Convey("Should be able to delete a dashboard folder and its children", func() {
|
||||
deleteCmd := &m.DeleteDashboardCommand{Id: savedFolder.Id}
|
||||
err := DeleteDashboard(deleteCmd)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
query := search.FindPersistedDashboardsQuery{
|
||||
OrgId: 1,
|
||||
FolderIds: []int64{savedFolder.Id},
|
||||
SignedInUser: &m.SignedInUser{},
|
||||
}
|
||||
|
||||
err = SearchDashboards(&query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(len(query.Result), ShouldEqual, 0)
|
||||
})
|
||||
|
||||
Convey("Should be able to get dashboard tags", func() {
|
||||
query := m.GetDashboardTagsQuery{OrgId: 1}
|
||||
|
||||
err := GetDashboardTags(&query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(len(query.Result), ShouldEqual, 2)
|
||||
})
|
||||
|
||||
Convey("Given two dashboards, one is starred dashboard by user 10, other starred by user 1", func() {
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -168,7 +168,12 @@ func addDashboardMigration(mg *Migrator) {
|
||||
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,
|
||||
}))
|
||||
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: '@',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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.createDashboardFolder(this.title).then(result => {
|
||||
appEvents.emit('alert-success', ['Folder Created', 'OK']);
|
||||
this.$location.url(result.meta.url);
|
||||
this.$location.url(locationUtil.stripBaseFromUrl(result.meta.url));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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} />
|
||||
|
||||
@@ -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]);
|
||||
})
|
||||
|
||||
@@ -22,7 +22,7 @@ describe('DashboardImportCtrl', function() {
|
||||
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() {
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
Old picker
|
||||
<user-picker user-picked="ctrl.userPicked($user)"></user-picker>
|
||||
-->
|
||||
<select-user-picker handlePicked="ctrl.userPicked" backendSrv="ctrl.backendSrv"></select-user-picker>
|
||||
<select-user-picker class="width-7" handlePicked="ctrl.userPicked" backendSrv="ctrl.backendSrv"></select-user-picker>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
@@ -100,7 +100,9 @@ module.directive('grafanaPanel', function($rootScope, $document, $timeout) {
|
||||
// update scrollbar after mounting
|
||||
ctrl.events.on('component-did-mount', () => {
|
||||
if (ctrl.__proto__.constructor.scrollable) {
|
||||
panelScrollbar = new PerfectScrollbar(panelContent[0]);
|
||||
panelScrollbar = new PerfectScrollbar(panelContent[0], {
|
||||
wheelPropagation: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ export class SoloPanelCtrl {
|
||||
|
||||
$scope.init = function() {
|
||||
contextSrv.sidemenu = false;
|
||||
appEvents.emit('toggle-sidemenu');
|
||||
appEvents.emit('toggle-sidemenu-hidden');
|
||||
|
||||
var params = $location.search();
|
||||
panelId = parseInt(params.panelId);
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<i class="icon-gf icon-gf-dashboard"></i>
|
||||
</td>
|
||||
<td>
|
||||
<a href="dashboard/{{dash.importedUri}}" ng-show="dash.imported">
|
||||
<a href="{{dash.importedUrl}}" ng-show="dash.imported">
|
||||
{{dash.title}}
|
||||
</a>
|
||||
<span ng-show="!dash.imported">
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
<empty-list-cta model="{
|
||||
title: 'There are no data sources defined yet',
|
||||
buttonIcon: 'gicon gicon-add-datasources',
|
||||
buttonLink: '/datasources/new',
|
||||
buttonLink: 'datasources/new',
|
||||
buttonTitle: 'Add data source',
|
||||
proTip: 'You can also define data sources through configuration files.',
|
||||
proTipLink: 'http://docs.grafana.org/administration/provisioning/#datasources?utm_source=grafana_ds_list',
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
<div ng-if="ctrl.queryModel.seriesByTagUsed" ng-repeat="tag in ctrl.queryModel.tags" class="gf-form">
|
||||
<gf-form-dropdown
|
||||
model="tag.key"
|
||||
lookup-text="false"
|
||||
allow-custom="true"
|
||||
label-mode="true"
|
||||
debounce="true"
|
||||
placeholder="Tag key"
|
||||
css-class="query-segment-key"
|
||||
get-options="ctrl.getTags($index, $query)"
|
||||
@@ -23,8 +23,6 @@
|
||||
/>
|
||||
<gf-form-dropdown
|
||||
model="tag.operator"
|
||||
lookup-text="false"
|
||||
allow-custom="false"
|
||||
label-mode="true"
|
||||
css-class="query-segment-operator"
|
||||
get-options="ctrl.getTagOperators()"
|
||||
@@ -33,9 +31,9 @@
|
||||
/>
|
||||
<gf-form-dropdown
|
||||
model="tag.value"
|
||||
lookup-text="false"
|
||||
allow-custom="true"
|
||||
label-mode="true"
|
||||
debounce="true"
|
||||
css-class="query-segment-value"
|
||||
placeholder="Tag value"
|
||||
get-options="ctrl.getTagValues(tag, $index, $query)"
|
||||
@@ -45,7 +43,7 @@
|
||||
</div>
|
||||
|
||||
<div ng-if="ctrl.queryModel.seriesByTagUsed" ng-repeat="segment in ctrl.addTagSegments" role="menuitem" class="gf-form">
|
||||
<metric-segment segment="segment" get-options="ctrl.getTagsAsSegments($query)" on-change="ctrl.addNewTag(segment)" />
|
||||
<metric-segment segment="segment" get-options="ctrl.getTagsAsSegments($query)" on-change="ctrl.addNewTag(segment)" debounce="true" />
|
||||
</div>
|
||||
|
||||
<div ng-if="!ctrl.queryModel.seriesByTagUsed" ng-repeat="segment in ctrl.segments" role="menuitem" class="gf-form">
|
||||
|
||||
@@ -246,6 +246,7 @@ module.directive('graphLegend', function(popoverSrv, $timeout) {
|
||||
// Number of pixels the content height can surpass the container height without enabling the scroll bar.
|
||||
scrollYMarginOffset: 2,
|
||||
suppressScrollX: true,
|
||||
wheelPropagation: true,
|
||||
};
|
||||
|
||||
if (!legendScrollbar) {
|
||||
|
||||
@@ -153,8 +153,12 @@ export class HeatmapTooltip {
|
||||
|
||||
getXBucketIndex(offsetX, data) {
|
||||
let x = this.scope.xScale.invert(offsetX - this.scope.yAxisWidth).valueOf();
|
||||
let xBucketIndex = getValueBucketBound(x, data.xBucketSize, 1);
|
||||
return xBucketIndex;
|
||||
// First try to find X bucket by checking x pos is in the
|
||||
// [bucket.x, bucket.x + xBucketSize] interval
|
||||
let xBucket = _.find(data.buckets, bucket => {
|
||||
return x > bucket.x && x - bucket.x <= data.xBucketSize;
|
||||
});
|
||||
return xBucket ? xBucket.x : getValueBucketBound(x, data.xBucketSize, 1);
|
||||
}
|
||||
|
||||
getYBucketIndex(offsetY, data) {
|
||||
|
||||
@@ -23,8 +23,7 @@ export class LoadDashboardCtrl {
|
||||
if (!($routeParams.type === 'script' || $routeParams.type === 'snapshot') && !$routeParams.uid) {
|
||||
backendSrv.get(`/api/dashboards/db/${$routeParams.slug}`).then(res => {
|
||||
if (res) {
|
||||
const url = locationUtil.stripBaseFromUrl(res.meta.url);
|
||||
$location.path(url).replace();
|
||||
$location.path(locationUtil.stripBaseFromUrl(res.meta.url)).replace();
|
||||
}
|
||||
});
|
||||
return;
|
||||
|
||||
@@ -53,6 +53,6 @@ export const FolderStore = types
|
||||
deleteFolder: flow(function* deleteFolder() {
|
||||
const backendSrv = getEnv(self).backendSrv;
|
||||
|
||||
return backendSrv.deleteDashboard(self.folder.url);
|
||||
return backendSrv.deleteDashboard(self.folder.uid);
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -108,6 +108,8 @@ export const PermissionsStore = types
|
||||
self.isFolder = isFolder;
|
||||
self.isInRoot = isInRoot;
|
||||
self.dashboardId = dashboardId;
|
||||
self.items.clear();
|
||||
|
||||
const res = yield backendSrv.get(`/api/dashboards/id/${dashboardId}/acl`);
|
||||
const items = prepareServerResponse(res, dashboardId, isFolder, isInRoot);
|
||||
self.items = items;
|
||||
|
||||
@@ -41,8 +41,15 @@
|
||||
|
||||
.theme-dark {
|
||||
.react-grid-item > .react-resizable-handle::after {
|
||||
border-right: 2px solid rgba(255, 255, 255, 0.4);
|
||||
border-bottom: 2px solid rgba(255, 255, 255, 0.4);
|
||||
border-right: 2px solid $gray-4;
|
||||
border-bottom: 2px solid $gray-4;
|
||||
}
|
||||
}
|
||||
|
||||
.theme-light {
|
||||
.react-grid-item > .react-resizable-handle::after {
|
||||
border-right: 2px solid $gray-3;
|
||||
border-bottom: 2px solid $gray-3;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
.dashlist-section {
|
||||
margin-bottom: $spacer;
|
||||
padding-top: 3px;
|
||||
}
|
||||
|
||||
.dashlist-link {
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
|
||||
<title>Grafana</title>
|
||||
<base href="[[.AppSubUrl]]/" />
|
||||
|
||||
<link rel="stylesheet" href="public/build/grafana.dark.min.css" title="Dark">
|
||||
<link rel="icon" type="image/png" href="public/img/fav32.png">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="gf-box" style="margin: 200px auto 0 auto; width: 500px;">
|
||||
<div class="gf-box-header">
|
||||
<span class="gf-box-title">
|
||||
Proxy authentication required
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="gf-box-body">
|
||||
<h4>Proxy authenticaion required</h4>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,39 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>Grafana - Error</title>
|
||||
|
||||
<base href="[[.AppSubUrl]]/" />
|
||||
|
||||
<link href='public/css/fonts.min.css' rel='stylesheet' type='text/css'>
|
||||
<link rel="stylesheet" href="public/build/grafana.dark.min.css">
|
||||
<link rel="icon" type="image/png" href="public/img/fav32.png">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="page-container">
|
||||
<div class="page-header">
|
||||
<h1>Server side error :(</h1>
|
||||
</div>
|
||||
<div class="panel-container" style="padding: 2rem">
|
||||
<div class="alert">
|
||||
<div class="alert-icon"><i class="fa fa-exclamation-triangle"></i></div>
|
||||
<div class="alert-body">
|
||||
<div class="alert-title">[[.Title]]</div>
|
||||
<div class="alert-text">
|
||||
[[if .ErrorMsg]]
|
||||
<pre>[[.ErrorMsg]]</pre>
|
||||
[[end]]
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding: 2rem 0 0">
|
||||
<p>Check the Grafana server logs for the detailed error message.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,57 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<meta name="theme-color" content="#000">
|
||||
|
||||
<title>Grafana - Error</title>
|
||||
|
||||
<base href="[[.AppSubUrl]]/" />
|
||||
|
||||
<link rel="stylesheet" href="public/build/grafana.[[ .Theme ]].css?v[[ .BuildVersion ]]">
|
||||
|
||||
<link rel="icon" type="image/png" href="public/img/fav32.png">
|
||||
<link rel="mask-icon" href="public/img/grafana_mask_icon.svg" color="#F05A28">
|
||||
|
||||
</head>
|
||||
|
||||
<body class="theme-[[ .Theme ]]">
|
||||
<div class="main-view">
|
||||
<div class="page-container">
|
||||
<div class="page-header">
|
||||
<div class="page-header__inner">
|
||||
<span class="page-header__logo">
|
||||
<i class="page-header__icon fa fa-frown-o"></i>
|
||||
</span>
|
||||
<div class="page-header__info-block">
|
||||
<h1 class="page-header__title">
|
||||
<a class="text-link" href="login">Grafana</a><span> / Server Error</span><span></span>
|
||||
</h1>
|
||||
<div class="page-header__sub-title">Sadly something went wrong</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-container page-body ng-scope" style="padding: 2rem">
|
||||
<div class="alert">
|
||||
<div class="alert-icon"><i class="fa fa-exclamation-triangle"></i></div>
|
||||
<div class="alert-body">
|
||||
<div class="alert-title">[[.Title]]</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
[[if .ErrorMsg]]
|
||||
<h4 class="page-heading">Error details</h4>
|
||||
<div class="alert-text">
|
||||
<pre>[[.ErrorMsg]]</pre>
|
||||
</div>
|
||||
[[end]]
|
||||
<div style="padding: 2rem 0 0">
|
||||
<p>Check the Grafana server logs for the detailed error message.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user