Commit e949eb3f by Torkel Ödegaard

Merge branch 'master' into dashboard-search-permissions-filter

parents 8e8f3c43 e429cd83
...@@ -80,8 +80,11 @@ In your custom.ini uncomment (remove the leading `;`) sign. And set `app_mode = ...@@ -80,8 +80,11 @@ In your custom.ini uncomment (remove the leading `;`) sign. And set `app_mode =
### Running tests ### Running tests
- You can run backend Golang tests using "go test ./pkg/...". #### Frontend
- Execute all frontend tests with "npm run test" Execute all frontend tests
```bash
npm run test
```
Writing & watching frontend tests (we have two test runners) Writing & watching frontend tests (we have two test runners)
...@@ -92,6 +95,18 @@ 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` - Start watcher: `npm run karma`
- Karma+Mocha runs all files that end with the name "_specs.ts". - 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 ## Contribute
If you have any idea for an improvement or found a bug, do not hesitate to open an issue. If you have any idea for an improvement or found a bug, do not hesitate to open an issue.
......
+++ +++
title = "Docs Home" title = "Grafana documentation"
description = "Install guide for Grafana" description = "Guides, Installation & Feature Documentation"
keywords = ["grafana", "installation", "documentation"] keywords = ["grafana", "installation", "documentation"]
type = "docs" type = "docs"
aliases = ["v1.1", "guides/reference/admin"] 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 <h2>Installing Grafana</h2>
visualizing time series data for infrastructure and application analytics but many use it in <div class="nav-cards">
other domains including industrial sensors, home automation, weather, and process control. <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 <h2>Guides</h2>
- [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)
For other platforms Read the [build from source]({{< relref "project/building_from_source.md" >}}) <div class="nav-cards">
instructions for more information. <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 <h2>Data Source Guides</h2>
<div class="nav-cards">
The back-end web server has a number of configuration options. Go the <a href="{{< relref "features/datasources/graphite.md" >}}" class="nav-cards__item nav-cards__item--ds">
[Configuration]({{< relref "installation/configuration.md" >}}) page for details on all <img src="/img/docs/logos/icon_graphite.svg" >
those options. <h5>Graphite</h5>
</a>
<a href="{{< relref "features/datasources/elasticsearch.md" >}}" class="nav-cards__item nav-cards__item--ds">
## Getting Started <img src="/img/docs/logos/icon_elasticsearch.svg" >
<h5>Elasticsearch</h5>
- [Getting Started]({{< relref "guides/getting_started.md" >}}) </a>
- [Basic Concepts]({{< relref "guides/basic_concepts.md" >}}) <a href="{{< relref "features/datasources/influxdb.md" >}}" class="nav-cards__item nav-cards__item--ds">
- [Screencasts]({{< relref "tutorials/screencasts.md" >}}) <img src="/img/docs/logos/icon_influxdb.svg" >
<h5>InfluxDB</h5>
## Data Source Guides </a>
<a href="{{< relref "features/datasources/prometheus.md" >}}" class="nav-cards__item nav-cards__item--ds">
- [Graphite]({{< relref "features/datasources/graphite.md" >}}) <img src="/img/docs/logos/icon_prometheus.svg" >
- [Elasticsearch]({{< relref "features/datasources/elasticsearch.md" >}}) <h5>Prometheus</h5>
- [InfluxDB]({{< relref "features/datasources/influxdb.md" >}}) </a>
- [Prometheus]({{< relref "features/datasources/prometheus.md" >}}) <a href="{{< relref "features/datasources/opentsdb.md" >}}" class="nav-cards__item nav-cards__item--ds">
- [OpenTSDB]({{< relref "features/datasources/opentsdb.md" >}}) <img src="/img/docs/logos/icon_opentsdb.png" >
- [MySQL]({{< relref "features/datasources/mysql.md" >}}) <h5>OpenTSDB</h5>
- [Postgres]({{< relref "features/datasources/postgres.md" >}}) </a>
- [Cloudwatch]({{< relref "features/datasources/cloudwatch.md" >}}) <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>
...@@ -5,6 +5,7 @@ import ( ...@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"os" "os"
"path" "path"
"strings"
"github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards"
...@@ -217,6 +218,10 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response { ...@@ -217,6 +218,10 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
return ApiError(400, m.ErrDashboardTitleEmpty.Error(), nil) 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 { if dash.Id == 0 {
limitReached, err := middleware.QuotaReached(c, "dashboard") limitReached, err := middleware.QuotaReached(c, "dashboard")
if err != nil { if err != nil {
...@@ -237,8 +242,11 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response { ...@@ -237,8 +242,11 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
dashboard, err := dashboards.GetRepository().SaveDashboard(dashItem) dashboard, err := dashboards.GetRepository().SaveDashboard(dashItem)
if err == m.ErrDashboardTitleEmpty { if err == m.ErrDashboardTitleEmpty ||
return ApiError(400, m.ErrDashboardTitleEmpty.Error(), nil) err == m.ErrDashboardWithSameNameAsFolder ||
err == m.ErrDashboardFolderWithSameNameAsDashboard ||
err == m.ErrDashboardTypeMismatch {
return ApiError(400, err.Error(), nil)
} }
if err == m.ErrDashboardContainsInvalidAlertData { if err == m.ErrDashboardContainsInvalidAlertData {
......
...@@ -46,26 +46,30 @@ func addOrgUserHelper(cmd m.AddOrgUserCommand) Response { ...@@ -46,26 +46,30 @@ func addOrgUserHelper(cmd m.AddOrgUserCommand) Response {
// GET /api/org/users // GET /api/org/users
func GetOrgUsersForCurrentOrg(c *middleware.Context) Response { 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 // GET /api/orgs/:orgId/users
func GetOrgUsers(c *middleware.Context) Response { func GetOrgUsers(c *middleware.Context) Response {
return getOrgUsersHelper(c.ParamsInt64(":orgId")) return getOrgUsersHelper(c.ParamsInt64(":orgId"), "", 0)
} }
func getOrgUsersHelper(orgId int64) Response { func getOrgUsersHelper(orgId int64, query string, limit int) Response {
query := m.GetOrgUsersQuery{OrgId: orgId} 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) 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) user.AvatarUrl = dtos.GetGravatarUrl(user.Email)
} }
return Json(200, query.Result) return Json(200, q.Result)
} }
// PATCH /api/org/users/:userId // PATCH /api/org/users/:userId
......
...@@ -13,17 +13,22 @@ import ( ...@@ -13,17 +13,22 @@ import (
// Typed errors // Typed errors
var ( var (
ErrDashboardNotFound = errors.New("Dashboard not found") ErrDashboardNotFound = errors.New("Dashboard not found")
ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found") ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found")
ErrDashboardWithSameUIDExists = errors.New("A dashboard with the same uid already exists") 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") 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") ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty") ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty")
ErrDashboardFolderCannotHaveParent = errors.New("A Dashboard Folder cannot be added to another folder") ErrDashboardFolderCannotHaveParent = errors.New("A Dashboard Folder cannot be added to another folder")
ErrDashboardContainsInvalidAlertData = errors.New("Invalid alert data. Cannot save dashboard") ErrDashboardContainsInvalidAlertData = errors.New("Invalid alert data. Cannot save dashboard")
ErrDashboardFailedToUpdateAlertData = errors.New("Failed to save alert data") ErrDashboardFailedToUpdateAlertData = errors.New("Failed to save alert data")
ErrDashboardsWithSameSlugExists = errors.New("Multiple dashboards with the same slug exists") ErrDashboardsWithSameSlugExists = errors.New("Multiple dashboards with the same slug exists")
ErrDashboardFailedGenerateUniqueUid = errors.New("Failed to generate unique dashboard id") 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 { type UpdatePluginDashboardError struct {
...@@ -95,14 +100,21 @@ func NewDashboardFromJson(data *simplejson.Json) *Dashboard { ...@@ -95,14 +100,21 @@ func NewDashboardFromJson(data *simplejson.Json) *Dashboard {
dash.Data = data dash.Data = data
dash.Title = dash.Data.Get("title").MustString() dash.Title = dash.Data.Get("title").MustString()
dash.UpdateSlug() dash.UpdateSlug()
update := false
if id, err := dash.Data.Get("id").Float64(); err == nil { if id, err := dash.Data.Get("id").Float64(); err == nil {
dash.Id = int64(id) dash.Id = int64(id)
update = true
}
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 { if version, err := dash.Data.Get("version").Float64(); err == nil && update {
dash.Version = int(version) dash.Version = int(version)
dash.Updated = time.Now() dash.Updated = time.Now()
}
} else { } else {
dash.Data.Set("version", 0) dash.Data.Set("version", 0)
dash.Created = time.Now() dash.Created = time.Now()
...@@ -113,10 +125,6 @@ func NewDashboardFromJson(data *simplejson.Json) *Dashboard { ...@@ -113,10 +125,6 @@ func NewDashboardFromJson(data *simplejson.Json) *Dashboard {
dash.GnetId = int64(gnetId) dash.GnetId = int64(gnetId)
} }
if uid, err := dash.Data.Get("uid").String(); err == nil {
dash.Uid = uid
}
return dash return dash
} }
......
...@@ -95,7 +95,10 @@ type UpdateOrgUserCommand struct { ...@@ -95,7 +95,10 @@ type UpdateOrgUserCommand struct {
// QUERIES // QUERIES
type GetOrgUsersQuery struct { type GetOrgUsersQuery struct {
OrgId int64 OrgId int64
Query string
Limit int
Result []*OrgUserDTO Result []*OrgUserDTO
} }
......
...@@ -82,6 +82,7 @@ func ImportDashboard(cmd *ImportDashboardCommand) error { ...@@ -82,6 +82,7 @@ func ImportDashboard(cmd *ImportDashboardCommand) error {
Path: cmd.Path, Path: cmd.Path,
Revision: dashboard.Data.Get("revision").MustInt64(1), Revision: dashboard.Data.Get("revision").MustInt64(1),
ImportedUri: "db/" + saveCmd.Result.Slug, ImportedUri: "db/" + saveCmd.Result.Slug,
ImportedUrl: saveCmd.Result.GetUrl(),
ImportedRevision: dashboard.Data.Get("revision").MustInt64(1), ImportedRevision: dashboard.Data.Get("revision").MustInt64(1),
Imported: true, Imported: true,
} }
......
...@@ -14,6 +14,7 @@ type PluginDashboardInfoDTO struct { ...@@ -14,6 +14,7 @@ type PluginDashboardInfoDTO struct {
Title string `json:"title"` Title string `json:"title"`
Imported bool `json:"imported"` Imported bool `json:"imported"`
ImportedUri string `json:"importedUri"` ImportedUri string `json:"importedUri"`
ImportedUrl string `json:"importedUrl"`
Slug string `json:"slug"` Slug string `json:"slug"`
DashboardId int64 `json:"dashboardId"` DashboardId int64 `json:"dashboardId"`
ImportedRevision int64 `json:"importedRevision"` ImportedRevision int64 `json:"importedRevision"`
...@@ -64,6 +65,7 @@ func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDT ...@@ -64,6 +65,7 @@ func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDT
res.DashboardId = existingDash.Id res.DashboardId = existingDash.Id
res.Imported = true res.Imported = true
res.ImportedUri = "db/" + existingDash.Slug res.ImportedUri = "db/" + existingDash.Slug
res.ImportedUrl = existingDash.GetUrl()
res.ImportedRevision = existingDash.Data.Get("revision").MustInt64(1) res.ImportedRevision = existingDash.Data.Get("revision").MustInt64(1)
existingMatches[existingDash.Id] = true existingMatches[existingDash.Id] = true
} }
......
...@@ -32,47 +32,36 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { ...@@ -32,47 +32,36 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
return inTransaction(func(sess *DBSession) error { return inTransaction(func(sess *DBSession) error {
dash := cmd.GetDashboardModel() dash := cmd.GetDashboardModel()
// try get existing dashboard if err := getExistingDashboardForUpdate(sess, dash, cmd); err != nil {
var existing m.Dashboard return err
}
if dash.Id != 0 { var existingByTitleAndFolder m.Dashboard
dashWithIdExists, err := sess.Where("id=? AND org_id=?", dash.Id, dash.OrgId).Get(&existing)
if err != nil {
return err
}
if !dashWithIdExists {
return m.ErrDashboardNotFound
}
// check for is someone else has written in between 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 dash.Version != existing.Version { if err != nil {
if cmd.Overwrite { return err
dash.Version = existing.Version }
} else {
return m.ErrDashboardVersionMismatch 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 !existingByTitleAndFolder.IsFolder && cmd.IsFolder {
if existing.PluginId != "" && cmd.Overwrite == false { return m.ErrDashboardFolderWithSameNameAsDashboard
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 sameUidExists { if cmd.Overwrite {
// another dashboard with same uid dash.Id = existingByTitleAndFolder.Id
if dash.Id != sameUid.Id { dash.Version = existingByTitleAndFolder.Version
if cmd.Overwrite {
dash.Id = sameUid.Id if dash.Uid == "" {
dash.Version = sameUid.Version dash.Uid = existingByTitleAndFolder.Uid
} else {
return m.ErrDashboardWithSameUIDExists
} }
} else {
return m.ErrDashboardWithSameNameInFolderExists
} }
} }
} }
...@@ -86,11 +75,6 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { ...@@ -86,11 +75,6 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
dash.Data.Set("uid", uid) dash.Data.Set("uid", uid)
} }
err := guaranteeDashboardNameIsUniqueInFolder(sess, dash)
if err != nil {
return err
}
err = setHasAcl(sess, dash) err = setHasAcl(sess, dash)
if err != nil { if err != nil {
return err return err
...@@ -162,6 +146,72 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { ...@@ -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) { func generateNewDashboardUid(sess *DBSession, orgId int64) (string, error) {
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
uid := generateNewUid() uid := generateNewUid()
...@@ -179,23 +229,6 @@ func generateNewDashboardUid(sess *DBSession, orgId int64) (string, error) { ...@@ -179,23 +229,6 @@ func generateNewDashboardUid(sess *DBSession, orgId int64) (string, error) {
return "", m.ErrDashboardFailedGenerateUniqueUid 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 { func setHasAcl(sess *DBSession, dash *m.Dashboard) error {
// check if parent has acl // check if parent has acl
if dash.FolderId > 0 { if dash.FolderId > 0 {
...@@ -472,9 +505,7 @@ func GetDashboardPermissionsForUser(query *m.GetDashboardPermissionsForUserQuery ...@@ -472,9 +505,7 @@ func GetDashboardPermissionsForUser(query *m.GetDashboardPermissionsForUserQuery
params = append(params, query.UserId) params = append(params, query.UserId)
params = append(params, dialect.BooleanStr(false)) params = append(params, dialect.BooleanStr(false))
x.ShowSQL(true)
err := x.Sql(sql, params...).Find(&query.Result) err := x.Sql(sql, params...).Find(&query.Result)
x.ShowSQL(false)
for _, p := range query.Result { for _, p := range query.Result {
p.PermissionName = p.Permission.String() p.PermissionName = p.Permission.String()
......
package sqlstore package sqlstore
import ( import (
"os"
"strings"
"testing" "testing"
"github.com/go-xorm/xorm" "github.com/go-xorm/xorm"
...@@ -11,10 +13,33 @@ import ( ...@@ -11,10 +13,33 @@ import (
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil" "github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
) )
var (
dbSqlite = "sqlite"
dbMySql = "mysql"
dbPostgres = "postgres"
)
func InitTestDB(t *testing.T) *xorm.Engine { func InitTestDB(t *testing.T) *xorm.Engine {
x, err := xorm.NewEngine(sqlutil.TestDB_Sqlite3.DriverName, sqlutil.TestDB_Sqlite3.ConnStr) selectedDb := dbSqlite
//x, err := xorm.NewEngine(sqlutil.TestDB_Mysql.DriverName, sqlutil.TestDB_Mysql.ConnStr) //selectedDb := dbMySql
//x, err := xorm.NewEngine(sqlutil.TestDB_Postgres.DriverName, sqlutil.TestDB_Postgres.ConnStr) //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() // x.ShowSQL()
......
...@@ -123,6 +123,31 @@ func TestAccountDataAccess(t *testing.T) { ...@@ -123,6 +123,31 @@ func TestAccountDataAccess(t *testing.T) {
So(query.Result[0].Role, ShouldEqual, "Admin") 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() { Convey("Can set using org", func() {
cmd := m.SetUsingOrgCommand{UserId: ac2.Id, OrgId: ac1.Id} cmd := m.SetUsingOrgCommand{UserId: ac2.Id, OrgId: ac1.Id}
err := SetUsingOrg(&cmd) err := SetUsingOrg(&cmd)
......
...@@ -2,6 +2,7 @@ package sqlstore ...@@ -2,6 +2,7 @@ package sqlstore
import ( import (
"fmt" "fmt"
"strings"
"time" "time"
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
...@@ -69,9 +70,30 @@ func UpdateOrgUser(cmd *m.UpdateOrgUserCommand) error { ...@@ -69,9 +70,30 @@ func UpdateOrgUser(cmd *m.UpdateOrgUserCommand) error {
func GetOrgUsers(query *m.GetOrgUsersQuery) error { func GetOrgUsers(query *m.GetOrgUsersQuery) error {
query.Result = make([]*m.OrgUserDTO, 0) query.Result = make([]*m.OrgUserDTO, 0)
sess := x.Table("org_user") sess := x.Table("org_user")
sess.Join("INNER", "user", fmt.Sprintf("org_user.user_id=%s.id", x.Dialect().Quote("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, "(user.email "+dialect.LikeStr()+" ? OR user.name "+dialect.LikeStr()+" ? OR user.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.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") sess.Asc("user.email", "user.login")
......
...@@ -31,7 +31,7 @@ class UserPicker extends Component<IProps, any> { ...@@ -31,7 +31,7 @@ class UserPicker extends Component<IProps, any> {
this.debouncedSearch = debounce(this.search, 300, { this.debouncedSearch = debounce(this.search, 300, {
leading: true, leading: true,
trailing: false, trailing: true,
}); });
} }
...@@ -39,10 +39,10 @@ class UserPicker extends Component<IProps, any> { ...@@ -39,10 +39,10 @@ class UserPicker extends Component<IProps, any> {
const { toggleLoading, backendSrv } = this.props; const { toggleLoading, backendSrv } = this.props;
toggleLoading(true); toggleLoading(true);
return backendSrv.get(`/api/users/search?perpage=10&page=1&query=${query}`).then(result => { return backendSrv.get(`/api/org/users?query=${query}&limit=10`).then(result => {
const users = result.users.map(user => { const users = result.map(user => {
return { return {
id: user.id, id: user.userId,
label: `${user.login} - ${user.email}`, label: `${user.login} - ${user.email}`,
avatarUrl: user.avatarUrl, avatarUrl: user.avatarUrl,
login: user.login, login: user.login,
......
...@@ -18,7 +18,7 @@ export class DashboardImportCtrl { ...@@ -18,7 +18,7 @@ export class DashboardImportCtrl {
nameValidationError: any; nameValidationError: any;
/** @ngInject */ /** @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.navModel = navModelSrv.getNav('create', 'import');
this.step = 1; this.step = 1;
...@@ -124,8 +124,7 @@ export class DashboardImportCtrl { ...@@ -124,8 +124,7 @@ export class DashboardImportCtrl {
inputs: inputs, inputs: inputs,
}) })
.then(res => { .then(res => {
this.$location.url('dashboard/' + res.importedUri); this.$location.url(res.importedUrl);
this.$scope.dismiss();
}); });
} }
......
...@@ -20,7 +20,10 @@ export class DashboardSrv { ...@@ -20,7 +20,10 @@ export class DashboardSrv {
return this.dash; return this.dash;
} }
handleSaveDashboardError(clone, err) { handleSaveDashboardError(clone, options, err) {
options = options || {};
options.overwrite = true;
if (err.data && err.data.status === 'version-mismatch') { if (err.data && err.data.status === 'version-mismatch') {
err.isHandled = true; err.isHandled = true;
...@@ -31,7 +34,7 @@ export class DashboardSrv { ...@@ -31,7 +34,7 @@ export class DashboardSrv {
yesText: 'Save & Overwrite', yesText: 'Save & Overwrite',
icon: 'fa-warning', icon: 'fa-warning',
onConfirm: () => { onConfirm: () => {
this.save(clone, { overwrite: true }); this.save(clone, options);
}, },
}); });
} }
...@@ -41,12 +44,12 @@ export class DashboardSrv { ...@@ -41,12 +44,12 @@ export class DashboardSrv {
this.$rootScope.appEvent('confirm-modal', { this.$rootScope.appEvent('confirm-modal', {
title: 'Conflict', 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?', text2: 'Would you still like to save this dashboard?',
yesText: 'Save & Overwrite', yesText: 'Save & Overwrite',
icon: 'fa-warning', icon: 'fa-warning',
onConfirm: () => { onConfirm: () => {
this.save(clone, { overwrite: true }); this.save(clone, options);
}, },
}); });
} }
...@@ -91,7 +94,7 @@ export class DashboardSrv { ...@@ -91,7 +94,7 @@ export class DashboardSrv {
return this.backendSrv return this.backendSrv
.saveDashboard(clone, options) .saveDashboard(clone, options)
.then(this.postSave.bind(this, clone)) .then(this.postSave.bind(this, clone))
.catch(this.handleSaveDashboardError.bind(this, clone)); .catch(this.handleSaveDashboardError.bind(this, clone, options));
} }
saveDashboard(options, clone) { saveDashboard(options, clone) {
......
...@@ -22,7 +22,7 @@ describe('DashboardImportCtrl', function() { ...@@ -22,7 +22,7 @@ describe('DashboardImportCtrl', function() {
validateNewDashboardName: jest.fn().mockReturnValue(Promise.resolve()), validateNewDashboardName: jest.fn().mockReturnValue(Promise.resolve()),
}; };
ctx.ctrl = new DashboardImportCtrl(backendSrv, validationSrv, navModelSrv, {}, {}, {}); ctx.ctrl = new DashboardImportCtrl(backendSrv, validationSrv, navModelSrv, {}, {});
}); });
describe('when uploading json', function() { describe('when uploading json', function() {
......
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
Old picker Old picker
<user-picker user-picked="ctrl.userPicked($user)"></user-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> </div>
</form> </form>
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
<i class="icon-gf icon-gf-dashboard"></i> <i class="icon-gf icon-gf-dashboard"></i>
</td> </td>
<td> <td>
<a href="dashboard/{{dash.importedUri}}" ng-show="dash.imported"> <a href="{{dash.importedUrl}}" ng-show="dash.imported">
{{dash.title}} {{dash.title}}
</a> </a>
<span ng-show="!dash.imported"> <span ng-show="!dash.imported">
......
...@@ -53,6 +53,6 @@ export const FolderStore = types ...@@ -53,6 +53,6 @@ export const FolderStore = types
deleteFolder: flow(function* deleteFolder() { deleteFolder: flow(function* deleteFolder() {
const backendSrv = getEnv(self).backendSrv; const backendSrv = getEnv(self).backendSrv;
return backendSrv.deleteDashboard(self.folder.url); return backendSrv.deleteDashboard(self.folder.uid);
}), }),
})); }));
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment