From fc05fc42c9ca54c4cb7b312366a40e30a03296e7 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Thu, 8 Feb 2018 10:52:13 +0100 Subject: [PATCH 1/9] added width class to add member choose (#10835) --- public/app/features/org/partials/team_details.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 --> - + From 0e8377a9f4cc736e59588c70b0128f66dea2ee01 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 8 Feb 2018 12:48:38 +0100 Subject: [PATCH 2/9] Update logic for create/update dashboard, validation and plugin dashboard links (#10809) * enables overwrite if dashboard allready exist in folder * dashboard: Don't allow creating a folder named General * dashboards: update logic for save/update dashboard No id and uid creates a new dashboard/folder. No id and uid, with an existing title in folder allows overwrite of dashboard. Id without uid, allows update of existing dashboard/folder without overwrite. Uid without id allows update of existing dashboard/folder without overwrite. Id without uid, with an existing title in folder allows overwrite of dashboard/folder and updated will have the uid of overwritten. Uid without id, with an existing title in folder allows overwrite of dashboard/folder and new will have the same uid as provided. Trying to change an existing folder to a dashboard yields error. Trying to change an existing dashboard to a folder yields error. * dashboards: include folder id when confirmed to save with overwrite * dashboards: fixes due to new url structure Return importedUrl property in response to importing dashboards and getting plugin dashboards and use this for redirects/links in the frontend. --- pkg/api/dashboard.go | 12 +- pkg/models/dashboards.go | 46 +- pkg/plugins/dashboard_importer.go | 1 + pkg/plugins/dashboards.go | 2 + pkg/services/sqlstore/dashboard.go | 149 +++-- pkg/services/sqlstore/dashboard_test.go | 624 +++++++++++------- .../dashboard/dashboard_import_ctrl.ts | 5 +- .../app/features/dashboard/dashboard_srv.ts | 13 +- .../specs/dashboard_import_ctrl.jest.ts | 2 +- .../plugins/import_list/import_list.html | 2 +- 10 files changed, 520 insertions(+), 336 deletions(-) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index d7676899eb2..1ecf16b8cb0 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/models/dashboards.go b/pkg/models/dashboards.go index 12216718b44..866d10850dc 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/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 be8b11b1f5b..1445d25432a 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 { @@ -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() 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/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/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}} From a91bd53a340570706fa8c7463578b4dccaa856c0 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 8 Feb 2018 14:25:23 +0100 Subject: [PATCH 3/9] dashboard: fix delete of folder from folder settings tab. Fixes #10821 --- public/app/stores/FolderStore/FolderStore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); }), })); From 8921b0b51703ef3c889f8a6682d50cda1f941664 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 8 Feb 2018 19:16:45 +0100 Subject: [PATCH 4/9] db test: allow use of env variable for database engine to run tests for --- pkg/services/sqlstore/datasource_test.go | 31 +++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) 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() From a86f2fa34b9ccd2e3f573238199200a53e9cf3b3 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 9 Feb 2018 10:42:37 +0100 Subject: [PATCH 5/9] user picker should only include users from current org (#10845) --- pkg/api/org_users.go | 18 +++++++------ pkg/models/org_user.go | 5 +++- pkg/services/sqlstore/org_test.go | 25 +++++++++++++++++++ pkg/services/sqlstore/org_users.go | 24 +++++++++++++++++- .../app/core/components/Picker/UserPicker.tsx | 8 +++--- 5 files changed, 67 insertions(+), 13 deletions(-) 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/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/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, From 41a6e7c2c86be7810032e40fe6816cf2c945493e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 9 Feb 2018 12:39:03 +0100 Subject: [PATCH 6/9] docs: updated docs landing page --- docs/sources/index.md | 134 ++++++++++++++++++++++++++++++------------ 1 file changed, 97 insertions(+), 37 deletions(-) diff --git a/docs/sources/index.md b/docs/sources/index.md index c1072db47a5..2a047b5257d 100644 --- a/docs/sources/index.md +++ b/docs/sources/index.md @@ -1,6 +1,6 @@ +++ -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"] @@ -8,42 +8,102 @@ aliases = ["v1.1", "guides/reference/admin"] # Welcome to the 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. +Grafana is an open source metric analytics & visualization platform. 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 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) +

Installing Grafana

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

Guides

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

+ From e672604835cbf0edca56d80ed3c4eecdccdf79fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 9 Feb 2018 12:53:07 +0100 Subject: [PATCH 7/9] docs: minor docs update --- docs/sources/index.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/sources/index.md b/docs/sources/index.md index 2a047b5257d..3c59b9baba0 100644 --- a/docs/sources/index.md +++ b/docs/sources/index.md @@ -6,9 +6,7 @@ type = "docs" aliases = ["v1.1", "guides/reference/admin"] +++ -# Welcome to the Grafana Documentation - -Grafana is an open source metric analytics & visualization platform. 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. +# Grafana Documentation

Installing Grafana