diff --git a/README.md b/README.md index 80a80b694c1..81fb1f8d42b 100644 --- a/README.md +++ b/README.md @@ -80,8 +80,11 @@ In your custom.ini uncomment (remove the leading `;`) sign. And set `app_mode = ### Running tests -- You can run backend Golang tests using "go test ./pkg/...". -- Execute all frontend tests with "npm run test" +#### Frontend +Execute all frontend tests +```bash +npm run test +``` Writing & watching frontend tests (we have two test runners) @@ -92,6 +95,18 @@ Writing & watching frontend tests (we have two test runners) - Start watcher: `npm run karma` - Karma+Mocha runs all files that end with the name "_specs.ts". +#### Backend +```bash +# Run Golang tests using sqlite3 as database (default) +go test ./pkg/... + +# Run Golang tests using mysql as database - convenient to use /docker/blocks/mysql_tests +GRAFANA_TEST_DB=mysql go test ./pkg/... + +# Run Golang tests using postgres as database - convenient to use /docker/blocks/postgres_tests +GRAFANA_TEST_DB=postgres go test ./pkg/... +``` + ## Contribute If you have any idea for an improvement or found a bug, do not hesitate to open an issue. diff --git a/docs/sources/index.md b/docs/sources/index.md index c1072db47a5..3c59b9baba0 100644 --- a/docs/sources/index.md +++ b/docs/sources/index.md @@ -1,49 +1,107 @@ +++ -title = "Docs Home" -description = "Install guide for Grafana" +title = "Grafana documentation" +description = "Guides, Installation & Feature Documentation" keywords = ["grafana", "installation", "documentation"] type = "docs" aliases = ["v1.1", "guides/reference/admin"] +++ -# Welcome to the Grafana Documentation +# Grafana Documentation -Grafana is an open source metric analytics & visualization suite. It is most commonly used for -visualizing time series data for infrastructure and application analytics but many use it in -other domains including industrial sensors, home automation, weather, and process control. +

Installing Grafana

+ -## 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) +

Guides

-For other platforms Read the [build from source]({{< relref "project/building_from_source.md" >}}) -instructions for more information. + -## 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" >}}) +

Data Source Guides

+ diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index a1f3560c0c6..4a00bd2861e 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -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 { diff --git a/pkg/api/org_users.go b/pkg/api/org_users.go index 57a15bd8db5..433b9f2bd66 100644 --- a/pkg/api/org_users.go +++ b/pkg/api/org_users.go @@ -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 diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index a91d4c4ed62..cbe97d06c57 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -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 } diff --git a/pkg/models/org_user.go b/pkg/models/org_user.go index 9379625d458..ca32cc50060 100644 --- a/pkg/models/org_user.go +++ b/pkg/models/org_user.go @@ -95,7 +95,10 @@ type UpdateOrgUserCommand struct { // QUERIES type GetOrgUsersQuery struct { - OrgId int64 + OrgId int64 + Query string + Limit int + Result []*OrgUserDTO } diff --git a/pkg/plugins/dashboard_importer.go b/pkg/plugins/dashboard_importer.go index bf516818e3c..9036b943b30 100644 --- a/pkg/plugins/dashboard_importer.go +++ b/pkg/plugins/dashboard_importer.go @@ -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, } diff --git a/pkg/plugins/dashboards.go b/pkg/plugins/dashboards.go index 37e3d8c0076..d15bcdd6db5 100644 --- a/pkg/plugins/dashboards.go +++ b/pkg/plugins/dashboards.go @@ -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 } diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index dd739343d7b..05cce3f001f 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -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 { @@ -472,9 +505,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() diff --git a/pkg/services/sqlstore/dashboard_test.go b/pkg/services/sqlstore/dashboard_test.go index 97ab5472d94..bd769d307eb 100644 --- a/pkg/services/sqlstore/dashboard_test.go +++ b/pkg/services/sqlstore/dashboard_test.go @@ -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() { diff --git a/pkg/services/sqlstore/datasource_test.go b/pkg/services/sqlstore/datasource_test.go index e6f0114ab4d..28f5b8eba9d 100644 --- a/pkg/services/sqlstore/datasource_test.go +++ b/pkg/services/sqlstore/datasource_test.go @@ -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() diff --git a/pkg/services/sqlstore/org_test.go b/pkg/services/sqlstore/org_test.go index 59d96c4f8ca..5322dfd4748 100644 --- a/pkg/services/sqlstore/org_test.go +++ b/pkg/services/sqlstore/org_test.go @@ -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) diff --git a/pkg/services/sqlstore/org_users.go b/pkg/services/sqlstore/org_users.go index 2c2a51fd362..79745c30386 100644 --- a/pkg/services/sqlstore/org_users.go +++ b/pkg/services/sqlstore/org_users.go @@ -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, "(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.Asc("user.email", "user.login") diff --git a/public/app/core/components/Picker/UserPicker.tsx b/public/app/core/components/Picker/UserPicker.tsx index 5c36505aeaa..77bf6c1fe15 100644 --- a/public/app/core/components/Picker/UserPicker.tsx +++ b/public/app/core/components/Picker/UserPicker.tsx @@ -31,7 +31,7 @@ class UserPicker extends Component { this.debouncedSearch = debounce(this.search, 300, { leading: true, - trailing: false, + trailing: true, }); } @@ -39,10 +39,10 @@ class UserPicker extends Component { 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, diff --git a/public/app/features/dashboard/dashboard_import_ctrl.ts b/public/app/features/dashboard/dashboard_import_ctrl.ts index c3af0a51a19..d127e628a77 100644 --- a/public/app/features/dashboard/dashboard_import_ctrl.ts +++ b/public/app/features/dashboard/dashboard_import_ctrl.ts @@ -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); }); } diff --git a/public/app/features/dashboard/dashboard_srv.ts b/public/app/features/dashboard/dashboard_srv.ts index 8d00dc4fc60..a1ff99007e3 100644 --- a/public/app/features/dashboard/dashboard_srv.ts +++ b/public/app/features/dashboard/dashboard_srv.ts @@ -20,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; @@ -31,7 +34,7 @@ export class DashboardSrv { yesText: 'Save & Overwrite', icon: 'fa-warning', onConfirm: () => { - this.save(clone, { overwrite: true }); + this.save(clone, options); }, }); } @@ -41,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); }, }); } @@ -91,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) { diff --git a/public/app/features/dashboard/specs/dashboard_import_ctrl.jest.ts b/public/app/features/dashboard/specs/dashboard_import_ctrl.jest.ts index 3d311b8b4f8..1cb59ef5bac 100644 --- a/public/app/features/dashboard/specs/dashboard_import_ctrl.jest.ts +++ b/public/app/features/dashboard/specs/dashboard_import_ctrl.jest.ts @@ -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() { diff --git a/public/app/features/org/partials/team_details.html b/public/app/features/org/partials/team_details.html index 9f06ebdb017..3fce8b3c720 100644 --- a/public/app/features/org/partials/team_details.html +++ b/public/app/features/org/partials/team_details.html @@ -33,7 +33,7 @@ Old picker --> - + diff --git a/public/app/features/plugins/import_list/import_list.html b/public/app/features/plugins/import_list/import_list.html index ff655f0c33a..fec7ba190ec 100644 --- a/public/app/features/plugins/import_list/import_list.html +++ b/public/app/features/plugins/import_list/import_list.html @@ -6,7 +6,7 @@ - + {{dash.title}} diff --git a/public/app/stores/FolderStore/FolderStore.ts b/public/app/stores/FolderStore/FolderStore.ts index e4533f6a3a9..46010475622 100644 --- a/public/app/stores/FolderStore/FolderStore.ts +++ b/public/app/stores/FolderStore/FolderStore.ts @@ -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); }), }));