diff --git a/CHANGELOG.md b/CHANGELOG.md index 901a2145e0f..6508c4ff76a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ # 5.0.0-beta1 (2018-02-05) -Grafana v5.0 is going to be the biggest and most foundational release Grafana has ever had, coming with a ton of UX improvements, a new dashboard grid engine, dashboard folders, user teams and permissions. Checkout out this [video preview](https://www.youtube.com/watch?v=BC_YRNpqj5k) of Grafana v5. +Grafana v5.0 is going to be the biggest and most foundational release Grafana has ever had, coming with a ton of UX improvements, a new dashboard grid engine, dashboard folders, user teams and permissions. Checkout out this [video preview](https://www.youtube.com/watch?v=Izr0IBgoTZQ) of Grafana v5. ### New Major Features - **Dashboards** Dashboard folders, [#1611](https://github.com/grafana/grafana/issues/1611) @@ -69,6 +69,17 @@ Dashboard panels and rows are positioned using a gridPos object `{x: 0, y: 0, w: ## Tech * **RabbitMq**: Remove support for publishing events to RabbitMQ [#9645](https://github.com/grafana/grafana/issues/9645) +## Deprecation notes + +### HTTP API +The following operations have been deprecated and will be removed in a future release: + - `GET /api/dashboards/db/:slug` -> Use `GET /api/dashboards/uid/:uid` instead + - `DELETE /api/dashboards/db/:slug` -> Use `DELETE /api/dashboards/uid/:uid` instead + +The following properties have been deprecated and will be removed in a future release: + - `uri` property in `GET /api/search` -> Use new `url` or `uid` property instead + - `meta.slug` property in `GET /api/dashboards/uid/:uid` and `GET /api/dashboards/db/:slug` -> Use new `meta.url` or `dashboard.uid` property instead + # 4.6.3 (2017-12-14) ## Fixes 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/guides/whats-new-in-v5.md b/docs/sources/guides/whats-new-in-v5.md index 0dd9337e404..bd960ed1694 100644 --- a/docs/sources/guides/whats-new-in-v5.md +++ b/docs/sources/guides/whats-new-in-v5.md @@ -12,7 +12,7 @@ weight = -6 # What's New in Grafana v5.0 -> Out in beta: [Download now!](https://www.youtube.com/watch?v=Izr0IBgoTZQ) +> Out in beta: [Download now!](https://grafana.com/grafana/download/5.0.0-beta1) This is the most substantial update that Grafana has ever seen. This article will detail the major new features and enhancements. @@ -24,10 +24,12 @@ This is the most substantial update that Grafana has ever seen. This article wil - [Group users into teams]({{< relref "#teams" >}}) and use them in the new permission system. - [Datasource provisioning]({{< relref "#data-sources" >}}) makes it possible to setup datasources via config files. - [Dashboard provisioning]({{< relref "#dashboards" >}}) makes it possible to setup dashboards via config files. +- [Persistent dashboard url's]({{< relref "#dashboard-model-persistent-url-s-and-api-changes" >}}) makes it possible to rename dashboards without breaking links. +- [Graphite Tags & Integrated Function Docs]({{< relref "#graphite-tags-integrated-function-docs" >}}). ### Video showing new features - +
## New Dashboard Layout Engine @@ -51,7 +53,7 @@ Almost every page has seen significant UX improvements. All pages (except dashbo
-### Dashboard Settings +## Dashboard Settings {{< docs-imagebox img="/img/docs/v50/dashboard_settings.png" max-width="1000px" class="docs-image--right" >}} Dashboard pages have a new header toolbar where buttons and actions are now all moved to the right. All the dashboard @@ -95,7 +97,7 @@ data sources a user can access nor what queries a user can issue.
-# Provisioning from configuration +## Provisioning from configuration In previous versions of Grafana, you could only use the API for provisioning data sources and dashboards. But that required the service to be running before you started creating dashboards and you also needed to @@ -117,17 +119,36 @@ in sync with dashboards in Grafana's database. The dashboard provisioner has mul which makes it possible to star them, use one as the home dashboard, set permissions and other features in Grafana that expects the dashboards to exist in the database. More info in the [dashboard provisioning docs](/administration/provisioning/#dashboards) -# Dashboard model, new url structure & API changes + +## Graphite Tags & Integrated Function Docs + +{{< docs-imagebox img="/img/docs/v50/graphite_tags.png" max-width="1000px" class="docs-image--right" >}} + +The Graphite query editor has been updated to support the latest Graphite version (v1.2) that adds +many new functions and support for querying by tags. You can now also view function documentation right in the query editor! + +Read more on [Graphite Tag Support](http://graphite.readthedocs.io/en/latest/tags.html?highlight=tags). + +
+ +## Dashboard model, persistent url's and API changes We are introducing a new unique identifier (`uid`) in the dashboard JSON model. It's automatically generated if not provided when creating a dashboard and will have a length of 9-12 characters. -The unique identifier allows having consistent URL's for accessing dashboards and sharing them -between instances. The new routes and API's for accessing dashboards will use the `uid` instead -of the `slug`. We'll keep supporting the old routes for accessing dashboards for backward -compatibility, but please note that we'll deprecate the old slug-based routes in the future. -This means that changing the title of dashboards will not break any bookmarked links. +The unique identifier allows having persistent URL's for accessing dashboards, sharing them +between instances and when using [dashboard provisioning](#dashboards). This means that dashboard can +be renamed without breaking any links. We're changing the url format for dashboards +from `/dashboard/db/:slug` to `/d/:uid/:slug`. We'll keep supporting the old slug-based url's for dashboards +and redirects to the new one for backward compatibility. Please note that the old slug-based url's +have been deprecated and will be removed in a future release. Sharing dashboards between instances becomes much easier since the `uid` is unique (unique enough). This might seem like a small change, but we are incredibly excited about it since it will make it much easier to manage, collaborate and navigate between dashboards. + +### API changes +New uid-based routes in the dashboard API have been introduced to retrieve and delete dashboards. +The corresponding slug-based routes have been deprecated and will be removed in a future release. + + 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/api.go b/pkg/api/api.go index 752af7602f5..ad77e410dbf 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -150,13 +150,13 @@ func (hs *HttpServer) registerRoutes() { apiRoute.Group("/teams", func(teamsRoute RouteRegister) { teamsRoute.Get("/:teamId", wrap(GetTeamById)) teamsRoute.Get("/search", wrap(SearchTeams)) - teamsRoute.Post("/", quota("teams"), reqOrgAdmin, bind(m.CreateTeamCommand{}), wrap(CreateTeam)) - teamsRoute.Put("/:teamId", reqOrgAdmin, bind(m.UpdateTeamCommand{}), wrap(UpdateTeam)) - teamsRoute.Delete("/:teamId", reqOrgAdmin, wrap(DeleteTeamById)) - teamsRoute.Get("/:teamId/members", reqOrgAdmin, wrap(GetTeamMembers)) - teamsRoute.Post("/:teamId/members", reqOrgAdmin, quota("teams"), bind(m.AddTeamMemberCommand{}), wrap(AddTeamMember)) - teamsRoute.Delete("/:teamId/members/:userId", reqOrgAdmin, wrap(RemoveTeamMember)) - }) + teamsRoute.Post("/", quota("teams"), bind(m.CreateTeamCommand{}), wrap(CreateTeam)) + teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), wrap(UpdateTeam)) + teamsRoute.Delete("/:teamId", wrap(DeleteTeamById)) + teamsRoute.Get("/:teamId/members", wrap(GetTeamMembers)) + teamsRoute.Post("/:teamId/members", quota("teams"), bind(m.AddTeamMemberCommand{}), wrap(AddTeamMember)) + teamsRoute.Delete("/:teamId/members/:userId", wrap(RemoveTeamMember)) + }, reqOrgAdmin) // org information available to all users. apiRoute.Group("/org", func(orgRoute RouteRegister) { 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/api/dashboard_acl.go b/pkg/api/dashboard_acl.go index b5d912d25f1..45f121dd0d0 100644 --- a/pkg/api/dashboard_acl.go +++ b/pkg/api/dashboard_acl.go @@ -13,6 +13,11 @@ import ( func GetDashboardAclList(c *middleware.Context) Response { dashId := c.ParamsInt64(":dashboardId") + _, rsp := getDashboardHelper(c.OrgId, "", dashId, "") + if rsp != nil { + return rsp + } + guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser) if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin { @@ -36,6 +41,11 @@ func GetDashboardAclList(c *middleware.Context) Response { func UpdateDashboardAcl(c *middleware.Context, apiCmd dtos.UpdateDashboardAclCommand) Response { dashId := c.ParamsInt64(":dashboardId") + _, rsp := getDashboardHelper(c.OrgId, "", dashId, "") + if rsp != nil { + return rsp + } + guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser) if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin { return dashboardGuardianResponse(err) @@ -79,6 +89,11 @@ func DeleteDashboardAcl(c *middleware.Context) Response { dashId := c.ParamsInt64(":dashboardId") aclId := c.ParamsInt64(":aclId") + _, rsp := getDashboardHelper(c.OrgId, "", dashId, "") + if rsp != nil { + return rsp + } + guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser) if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin { return dashboardGuardianResponse(err) diff --git a/pkg/api/dashboard_acl_test.go b/pkg/api/dashboard_acl_test.go index 467045e360a..e43e57ed5c0 100644 --- a/pkg/api/dashboard_acl_test.go +++ b/pkg/api/dashboard_acl_test.go @@ -23,6 +23,14 @@ func TestDashboardAclApiEndpoint(t *testing.T) { } dtoRes := transformDashboardAclsToDTOs(mockResult) + getDashboardQueryResult := m.NewDashboard("Dash") + var getDashboardNotFoundError error + + bus.AddHandler("test", func(query *m.GetDashboardQuery) error { + query.Result = getDashboardQueryResult + return getDashboardNotFoundError + }) + bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error { query.Result = dtoRes return nil @@ -60,6 +68,40 @@ func TestDashboardAclApiEndpoint(t *testing.T) { So(respJSON.GetIndex(0).Get("permission").MustInt(), ShouldEqual, m.PERMISSION_VIEW) }) }) + + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_ADMIN, func(sc *scenarioContext) { + getDashboardNotFoundError = m.ErrDashboardNotFound + sc.handlerFunc = GetDashboardAclList + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + + Convey("Should not be able to access ACL", func() { + So(sc.resp.Code, ShouldEqual, 404) + }) + }) + + Convey("Should not be able to update permissions for non-existing dashboard", func() { + cmd := dtos.UpdateDashboardAclCommand{ + Items: []dtos.DashboardAclUpdateItem{ + {UserId: 1000, Permission: m.PERMISSION_ADMIN}, + }, + } + + postAclScenario("When calling POST on", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_ADMIN, cmd, func(sc *scenarioContext) { + getDashboardNotFoundError = m.ErrDashboardNotFound + CallPostAcl(sc) + So(sc.resp.Code, ShouldEqual, 404) + }) + }) + + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/2/acl/6", "/api/dashboards/id/:dashboardId/acl/:aclId", m.ROLE_ADMIN, func(sc *scenarioContext) { + getDashboardNotFoundError = m.ErrDashboardNotFound + sc.handlerFunc = DeleteDashboardAcl + sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() + + Convey("Should not be able to delete non-existing dashboard", func() { + So(sc.resp.Code, ShouldEqual, 404) + }) + }) }) Convey("When user is org editor and has admin permission in the ACL", func() { diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go index 22fcafc8b29..341ff212f10 100644 --- a/pkg/api/login_oauth.go +++ b/pkg/api/login_oauth.go @@ -40,14 +40,14 @@ func GenStateString() string { func OAuthLogin(ctx *middleware.Context) { if setting.OAuthService == nil { - ctx.Handle(404, "login.OAuthLogin(oauth service not enabled)", nil) + ctx.Handle(404, "OAuth not enabled", nil) return } name := ctx.Params(":name") connect, ok := social.SocialMap[name] if !ok { - ctx.Handle(404, "login.OAuthLogin(social login not enabled)", errors.New(name)) + ctx.Handle(404, fmt.Sprintf("No OAuth with name %s configured", name), nil) return } 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/api/team.go b/pkg/api/team.go index af537224d41..f11eca68b91 100644 --- a/pkg/api/team.go +++ b/pkg/api/team.go @@ -26,6 +26,7 @@ func CreateTeam(c *middleware.Context, cmd m.CreateTeamCommand) Response { // PUT /api/teams/:teamId func UpdateTeam(c *middleware.Context, cmd m.UpdateTeamCommand) Response { + cmd.OrgId = c.OrgId cmd.Id = c.ParamsInt64(":teamId") if err := bus.Dispatch(&cmd); err != nil { if err == m.ErrTeamNameTaken { @@ -39,7 +40,7 @@ func UpdateTeam(c *middleware.Context, cmd m.UpdateTeamCommand) Response { // DELETE /api/teams/:teamId func DeleteTeamById(c *middleware.Context) Response { - if err := bus.Dispatch(&m.DeleteTeamCommand{Id: c.ParamsInt64(":teamId")}); err != nil { + if err := bus.Dispatch(&m.DeleteTeamCommand{OrgId: c.OrgId, Id: c.ParamsInt64(":teamId")}); err != nil { if err == m.ErrTeamNotFound { return ApiError(404, "Failed to delete Team. ID not found", nil) } @@ -60,11 +61,11 @@ func SearchTeams(c *middleware.Context) Response { } query := m.SearchTeamsQuery{ + OrgId: c.OrgId, Query: c.Query("query"), Name: c.Query("name"), Page: page, Limit: perPage, - OrgId: c.OrgId, } if err := bus.Dispatch(&query); err != nil { @@ -83,7 +84,7 @@ func SearchTeams(c *middleware.Context) Response { // GET /api/teams/:teamId func GetTeamById(c *middleware.Context) Response { - query := m.GetTeamByIdQuery{Id: c.ParamsInt64(":teamId")} + query := m.GetTeamByIdQuery{OrgId: c.OrgId, Id: c.ParamsInt64(":teamId")} if err := bus.Dispatch(&query); err != nil { if err == m.ErrTeamNotFound { diff --git a/pkg/api/team_members.go b/pkg/api/team_members.go index 412e142edb7..59dfc20b791 100644 --- a/pkg/api/team_members.go +++ b/pkg/api/team_members.go @@ -10,7 +10,7 @@ import ( // GET /api/teams/:teamId/members func GetTeamMembers(c *middleware.Context) Response { - query := m.GetTeamMembersQuery{TeamId: c.ParamsInt64(":teamId")} + query := m.GetTeamMembersQuery{OrgId: c.OrgId, TeamId: c.ParamsInt64(":teamId")} if err := bus.Dispatch(&query); err != nil { return ApiError(500, "Failed to get Team Members", err) @@ -42,7 +42,7 @@ func AddTeamMember(c *middleware.Context, cmd m.AddTeamMemberCommand) Response { // DELETE /api/teams/:teamId/members/:userId func RemoveTeamMember(c *middleware.Context) Response { - if err := bus.Dispatch(&m.RemoveTeamMemberCommand{TeamId: c.ParamsInt64(":teamId"), UserId: c.ParamsInt64(":userId")}); err != nil { + if err := bus.Dispatch(&m.RemoveTeamMemberCommand{OrgId: c.OrgId, TeamId: c.ParamsInt64(":teamId"), UserId: c.ParamsInt64(":userId")}); err != nil { return ApiError(500, "Failed to remove Member from Team", err) } return ApiSuccess("Team Member removed") diff --git a/pkg/middleware/auth.go b/pkg/middleware/auth.go index be3415d990b..826287e12f3 100644 --- a/pkg/middleware/auth.go +++ b/pkg/middleware/auth.go @@ -42,8 +42,7 @@ func accessForbidden(c *Context) { return } - c.SetCookie("redirect_to", url.QueryEscape(setting.AppSubUrl+c.Req.RequestURI), 0, setting.AppSubUrl+"/") - c.Redirect(setting.AppSubUrl + "/login") + c.Redirect(setting.AppSubUrl + "/") } func notAuthorized(c *Context) { diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index 259d800f0a9..f0c952811cd 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -206,7 +206,9 @@ func (ctx *Context) Handle(status int, title string, err error) { ctx.Data["Title"] = title ctx.Data["AppSubUrl"] = setting.AppSubUrl - ctx.HTML(status, strconv.Itoa(status)) + ctx.Data["Theme"] = "dark" + + ctx.HTML(status, "error") } func (ctx *Context) JsonOK(message string) { diff --git a/pkg/middleware/recovery.go b/pkg/middleware/recovery.go index 0c9dc4670e2..388acc15afc 100644 --- a/pkg/middleware/recovery.go +++ b/pkg/middleware/recovery.go @@ -137,7 +137,7 @@ func Recovery() macaron.Handler { c.JSON(500, resp) } else { - c.HTML(500, "500") + c.HTML(500, "error") } } }() 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/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/models/team.go b/pkg/models/team.go index d2912f431b8..f789f125aa1 100644 --- a/pkg/models/team.go +++ b/pkg/models/team.go @@ -37,18 +37,22 @@ type UpdateTeamCommand struct { Id int64 Name string Email string + OrgId int64 `json:"-"` } type DeleteTeamCommand struct { - Id int64 + OrgId int64 + Id int64 } type GetTeamByIdQuery struct { + OrgId int64 Id int64 Result *Team } type GetTeamsByUserQuery struct { + OrgId int64 UserId int64 `json:"userId"` Result []*Team `json:"teams"` } diff --git a/pkg/models/team_member.go b/pkg/models/team_member.go index 9970678a1ae..19cf657292d 100644 --- a/pkg/models/team_member.go +++ b/pkg/models/team_member.go @@ -31,6 +31,7 @@ type AddTeamMemberCommand struct { } type RemoveTeamMemberCommand struct { + OrgId int64 `json:"-"` UserId int64 TeamId int64 } @@ -39,6 +40,7 @@ type RemoveTeamMemberCommand struct { // QUERIES type GetTeamMembersQuery struct { + OrgId int64 TeamId int64 Result []*TeamMemberDTO } 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/guardian/guardian.go b/pkg/services/guardian/guardian.go index f4056841c33..b448561494d 100644 --- a/pkg/services/guardian/guardian.go +++ b/pkg/services/guardian/guardian.go @@ -160,7 +160,7 @@ func (g *DashboardGuardian) getTeams() ([]*m.Team, error) { return g.groups, nil } - query := m.GetTeamsByUserQuery{UserId: g.user.UserId} + query := m.GetTeamsByUserQuery{OrgId: g.orgId, UserId: g.user.UserId} err := bus.Dispatch(&query) g.groups = query.Result diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index af87c324216..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 { @@ -363,10 +396,10 @@ func GetFoldersForSignedInUser(query *m.GetFoldersForSignedInUserQuery) error { if query.SignedInUser.OrgRole == m.ROLE_ADMIN { sql := `SELECT distinct d.id, d.title - FROM dashboard AS d WHERE d.is_folder = ? + FROM dashboard AS d WHERE d.is_folder = ? AND d.org_id = ? ORDER BY d.title ASC` - err = x.Sql(sql, dialect.BooleanStr(true)).Find(&query.Result) + err = x.Sql(sql, dialect.BooleanStr(true), query.OrgId).Find(&query.Result) } else { params := make([]interface{}, 0) sql := `SELECT distinct d.id, d.title @@ -518,9 +551,7 @@ func GetDashboardPermissionsForUser(query *m.GetDashboardPermissionsForUserQuery params = append(params, query.UserId) params = append(params, dialect.BooleanStr(false)) - x.ShowSQL(true) err := x.Sql(sql, params...).Find(&query.Result) - x.ShowSQL(false) for _, p := range query.Result { p.PermissionName = p.Permission.String() diff --git a/pkg/services/sqlstore/dashboard_folder_test.go b/pkg/services/sqlstore/dashboard_folder_test.go index 10a55f9b22b..4818deaae14 100644 --- a/pkg/services/sqlstore/dashboard_folder_test.go +++ b/pkg/services/sqlstore/dashboard_folder_test.go @@ -219,13 +219,14 @@ func TestDashboardFolderDataAccess(t *testing.T) { folder1 := insertTestDashboard("1 test dash folder", 1, 0, true, "prod") folder2 := insertTestDashboard("2 test dash folder", 1, 0, true, "prod") + insertTestDashboard("folder in another org", 2, 0, true, "prod") adminUser := createUser("admin", "Admin", true) editorUser := createUser("editor", "Editor", false) viewerUser := createUser("viewer", "Viewer", false) Convey("Admin users", func() { - Convey("Should have write access to all dashboard folders", func() { + Convey("Should have write access to all dashboard folders in their org", func() { query := m.GetFoldersForSignedInUserQuery{ OrgId: 1, SignedInUser: &m.SignedInUser{UserId: adminUser.Id, OrgRole: m.ROLE_ADMIN}, 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/migrations/dashboard_mig.go b/pkg/services/sqlstore/migrations/dashboard_mig.go index bb2aea4bd51..c87a2906652 100644 --- a/pkg/services/sqlstore/migrations/dashboard_mig.go +++ b/pkg/services/sqlstore/migrations/dashboard_mig.go @@ -168,7 +168,12 @@ func addDashboardMigration(mg *Migrator) { Cols: []string{"org_id", "slug"}, Type: UniqueIndex, })) + mg.AddMigration("Update dashboard title length", NewTableCharsetMigration("dashboard", []*Column{ + {Name: "title", Type: DB_NVarchar, Length: 189, Nullable: false}, + })) + mg.AddMigration("Add unique index for dashboard_org_id_title_folder_id", NewAddIndexMigration(dashboardV2, &Index{ Cols: []string{"org_id", "folder_id", "title"}, Type: UniqueIndex, })) + } 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..0b991c73c55 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, "(email "+dialect.LikeStr()+" ? OR name "+dialect.LikeStr()+" ? OR login "+dialect.LikeStr()+" ?)") + whereParams = append(whereParams, queryWithWildcards, queryWithWildcards, queryWithWildcards) + } + + if len(whereConditions) > 0 { + sess.Where(strings.Join(whereConditions, " AND "), whereParams...) + } + + if query.Limit > 0 { + sess.Limit(query.Limit, 0) + } + sess.Cols("org_user.org_id", "org_user.user_id", "user.email", "user.login", "org_user.role", "user.last_seen_at") sess.Asc("user.email", "user.login") diff --git a/pkg/services/sqlstore/team.go b/pkg/services/sqlstore/team.go index 98bb1a36eb9..ecb34ad927b 100644 --- a/pkg/services/sqlstore/team.go +++ b/pkg/services/sqlstore/team.go @@ -25,7 +25,7 @@ func init() { func CreateTeam(cmd *m.CreateTeamCommand) error { return inTransaction(func(sess *DBSession) error { - if isNameTaken, err := isTeamNameTaken(cmd.Name, 0, sess); err != nil { + if isNameTaken, err := isTeamNameTaken(cmd.OrgId, cmd.Name, 0, sess); err != nil { return err } else if isNameTaken { return m.ErrTeamNameTaken @@ -50,7 +50,7 @@ func CreateTeam(cmd *m.CreateTeamCommand) error { func UpdateTeam(cmd *m.UpdateTeamCommand) error { return inTransaction(func(sess *DBSession) error { - if isNameTaken, err := isTeamNameTaken(cmd.Name, cmd.Id, sess); err != nil { + if isNameTaken, err := isTeamNameTaken(cmd.OrgId, cmd.Name, cmd.Id, sess); err != nil { return err } else if isNameTaken { return m.ErrTeamNameTaken @@ -80,20 +80,20 @@ func UpdateTeam(cmd *m.UpdateTeamCommand) error { func DeleteTeam(cmd *m.DeleteTeamCommand) error { return inTransaction(func(sess *DBSession) error { - if res, err := sess.Query("SELECT 1 from team WHERE id=?", cmd.Id); err != nil { + if res, err := sess.Query("SELECT 1 from team WHERE org_id=? and id=?", cmd.OrgId, cmd.Id); err != nil { return err } else if len(res) != 1 { return m.ErrTeamNotFound } deletes := []string{ - "DELETE FROM team_member WHERE team_id = ?", - "DELETE FROM team WHERE id = ?", - "DELETE FROM dashboard_acl WHERE team_id = ?", + "DELETE FROM team_member WHERE org_id=? and team_id = ?", + "DELETE FROM team WHERE org_id=? and id = ?", + "DELETE FROM dashboard_acl WHERE org_id=? and team_id = ?", } for _, sql := range deletes { - _, err := sess.Exec(sql, cmd.Id) + _, err := sess.Exec(sql, cmd.OrgId, cmd.Id) if err != nil { return err } @@ -102,9 +102,9 @@ func DeleteTeam(cmd *m.DeleteTeamCommand) error { }) } -func isTeamNameTaken(name string, existingId int64, sess *DBSession) (bool, error) { +func isTeamNameTaken(orgId int64, name string, existingId int64, sess *DBSession) (bool, error) { var team m.Team - exists, err := sess.Where("name=?", name).Get(&team) + exists, err := sess.Where("org_id=? and name=?", orgId, name).Get(&team) if err != nil { return false, nil @@ -128,6 +128,7 @@ func SearchTeams(query *m.SearchTeamsQuery) error { sql.WriteString(`select team.id as id, + team.org_id, team.name as name, team.email as email, (select count(*) from team_member where team_member.team_id = team.id) as member_count @@ -176,7 +177,7 @@ func SearchTeams(query *m.SearchTeamsQuery) error { func GetTeamById(query *m.GetTeamByIdQuery) error { var team m.Team - exists, err := x.Id(query.Id).Get(&team) + exists, err := x.Where("org_id=? and id=?", query.OrgId, query.Id).Get(&team) if err != nil { return err } @@ -194,7 +195,7 @@ func GetTeamsByUser(query *m.GetTeamsByUserQuery) error { sess := x.Table("team") sess.Join("INNER", "team_member", "team.id=team_member.team_id") - sess.Where("team_member.user_id=?", query.UserId) + sess.Where("team.org_id=? and team_member.user_id=?", query.OrgId, query.UserId) err := sess.Find(&query.Result) if err != nil { @@ -206,13 +207,13 @@ func GetTeamsByUser(query *m.GetTeamsByUserQuery) error { func AddTeamMember(cmd *m.AddTeamMemberCommand) error { return inTransaction(func(sess *DBSession) error { - if res, err := sess.Query("SELECT 1 from team_member WHERE team_id=? and user_id=?", cmd.TeamId, cmd.UserId); err != nil { + if res, err := sess.Query("SELECT 1 from team_member WHERE org_id=? and team_id=? and user_id=?", cmd.OrgId, cmd.TeamId, cmd.UserId); err != nil { return err } else if len(res) == 1 { return m.ErrTeamMemberAlreadyAdded } - if res, err := sess.Query("SELECT 1 from team WHERE id=?", cmd.TeamId); err != nil { + if res, err := sess.Query("SELECT 1 from team WHERE org_id=? and id=?", cmd.OrgId, cmd.TeamId); err != nil { return err } else if len(res) != 1 { return m.ErrTeamNotFound @@ -233,8 +234,8 @@ func AddTeamMember(cmd *m.AddTeamMemberCommand) error { func RemoveTeamMember(cmd *m.RemoveTeamMemberCommand) error { return inTransaction(func(sess *DBSession) error { - var rawSql = "DELETE FROM team_member WHERE team_id=? and user_id=?" - _, err := sess.Exec(rawSql, cmd.TeamId, cmd.UserId) + var rawSql = "DELETE FROM team_member WHERE org_id=? and team_id=? and user_id=?" + _, err := sess.Exec(rawSql, cmd.OrgId, cmd.TeamId, cmd.UserId) if err != nil { return err } @@ -247,7 +248,7 @@ func GetTeamMembers(query *m.GetTeamMembersQuery) error { query.Result = make([]*m.TeamMemberDTO, 0) sess := x.Table("team_member") sess.Join("INNER", "user", fmt.Sprintf("team_member.user_id=%s.id", x.Dialect().Quote("user"))) - sess.Where("team_member.team_id=?", query.TeamId) + sess.Where("team_member.org_id=? and team_member.team_id=?", query.OrgId, query.TeamId) sess.Cols("user.org_id", "team_member.team_id", "team_member.user_id", "user.email", "user.login") sess.Asc("user.login", "user.email") diff --git a/pkg/services/sqlstore/team_test.go b/pkg/services/sqlstore/team_test.go index dbae4545266..bebe59f4238 100644 --- a/pkg/services/sqlstore/team_test.go +++ b/pkg/services/sqlstore/team_test.go @@ -27,8 +27,9 @@ func TestTeamCommandsAndQueries(t *testing.T) { userIds = append(userIds, userCmd.Result.Id) } - group1 := m.CreateTeamCommand{Name: "group1 name", Email: "test1@test.com"} - group2 := m.CreateTeamCommand{Name: "group2 name", Email: "test2@test.com"} + var testOrgId int64 = 1 + group1 := m.CreateTeamCommand{OrgId: testOrgId, Name: "group1 name", Email: "test1@test.com"} + group2 := m.CreateTeamCommand{OrgId: testOrgId, Name: "group2 name", Email: "test2@test.com"} err := CreateTeam(&group1) So(err, ShouldBeNil) @@ -36,7 +37,7 @@ func TestTeamCommandsAndQueries(t *testing.T) { So(err, ShouldBeNil) Convey("Should be able to create teams and add users", func() { - query := &m.SearchTeamsQuery{Name: "group1 name", Page: 1, Limit: 10} + query := &m.SearchTeamsQuery{OrgId: testOrgId, Name: "group1 name", Page: 1, Limit: 10} err = SearchTeams(query) So(err, ShouldBeNil) So(query.Page, ShouldEqual, 1) @@ -44,25 +45,27 @@ func TestTeamCommandsAndQueries(t *testing.T) { team1 := query.Result.Teams[0] So(team1.Name, ShouldEqual, "group1 name") So(team1.Email, ShouldEqual, "test1@test.com") + So(team1.OrgId, ShouldEqual, testOrgId) - err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: 1, TeamId: team1.Id, UserId: userIds[0]}) + err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: team1.Id, UserId: userIds[0]}) So(err, ShouldBeNil) - q1 := &m.GetTeamMembersQuery{TeamId: team1.Id} + q1 := &m.GetTeamMembersQuery{OrgId: testOrgId, TeamId: team1.Id} err = GetTeamMembers(q1) So(err, ShouldBeNil) So(q1.Result[0].TeamId, ShouldEqual, team1.Id) So(q1.Result[0].Login, ShouldEqual, "loginuser0") + So(q1.Result[0].OrgId, ShouldEqual, testOrgId) }) Convey("Should be able to search for teams", func() { - query := &m.SearchTeamsQuery{Query: "group", Page: 1} + query := &m.SearchTeamsQuery{OrgId: testOrgId, Query: "group", Page: 1} err = SearchTeams(query) So(err, ShouldBeNil) So(len(query.Result.Teams), ShouldEqual, 2) So(query.Result.TotalCount, ShouldEqual, 2) - query2 := &m.SearchTeamsQuery{Query: ""} + query2 := &m.SearchTeamsQuery{OrgId: testOrgId, Query: ""} err = SearchTeams(query2) So(err, ShouldBeNil) So(len(query2.Result.Teams), ShouldEqual, 2) @@ -70,9 +73,9 @@ func TestTeamCommandsAndQueries(t *testing.T) { Convey("Should be able to return all teams a user is member of", func() { groupId := group2.Result.Id - err := AddTeamMember(&m.AddTeamMemberCommand{OrgId: 1, TeamId: groupId, UserId: userIds[0]}) + err := AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: groupId, UserId: userIds[0]}) - query := &m.GetTeamsByUserQuery{UserId: userIds[0]} + query := &m.GetTeamsByUserQuery{OrgId: testOrgId, UserId: userIds[0]} err = GetTeamsByUser(query) So(err, ShouldBeNil) So(len(query.Result), ShouldEqual, 1) @@ -81,7 +84,7 @@ func TestTeamCommandsAndQueries(t *testing.T) { }) Convey("Should be able to remove users from a group", func() { - err = RemoveTeamMember(&m.RemoveTeamMemberCommand{TeamId: group1.Result.Id, UserId: userIds[0]}) + err = RemoveTeamMember(&m.RemoveTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0]}) So(err, ShouldBeNil) q1 := &m.GetTeamMembersQuery{TeamId: group1.Result.Id} @@ -92,20 +95,20 @@ func TestTeamCommandsAndQueries(t *testing.T) { Convey("Should be able to remove a group with users and permissions", func() { groupId := group2.Result.Id - err := AddTeamMember(&m.AddTeamMemberCommand{OrgId: 1, TeamId: groupId, UserId: userIds[1]}) + err := AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: groupId, UserId: userIds[1]}) So(err, ShouldBeNil) - err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: 1, TeamId: groupId, UserId: userIds[2]}) + err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: groupId, UserId: userIds[2]}) So(err, ShouldBeNil) - err = SetDashboardAcl(&m.SetDashboardAclCommand{DashboardId: 1, OrgId: 1, Permission: m.PERMISSION_EDIT, TeamId: groupId}) + err = SetDashboardAcl(&m.SetDashboardAclCommand{DashboardId: 1, OrgId: testOrgId, Permission: m.PERMISSION_EDIT, TeamId: groupId}) - err = DeleteTeam(&m.DeleteTeamCommand{Id: groupId}) + err = DeleteTeam(&m.DeleteTeamCommand{OrgId: testOrgId, Id: groupId}) So(err, ShouldBeNil) - query := &m.GetTeamByIdQuery{Id: groupId} + query := &m.GetTeamByIdQuery{OrgId: testOrgId, Id: groupId} err = GetTeamById(query) So(err, ShouldEqual, m.ErrTeamNotFound) - permQuery := &m.GetDashboardAclInfoListQuery{DashboardId: 1, OrgId: 1} + permQuery := &m.GetDashboardAclInfoListQuery{DashboardId: 1, OrgId: testOrgId} err = GetDashboardAclInfoList(permQuery) So(err, ShouldBeNil) diff --git a/pkg/social/github_oauth.go b/pkg/social/github_oauth.go index 6f00cd2cd9e..c74e8825bc1 100644 --- a/pkg/social/github_oauth.go +++ b/pkg/social/github_oauth.go @@ -210,7 +210,7 @@ func (s *SocialGithub) UserInfo(client *http.Client, token *oauth2.Token) (*Basi if err != nil { return nil, fmt.Errorf("Error getting user info: %s", err) } - + data.OrganizationsUrl = s.apiUrl + "/user/orgs" userInfo := &BasicUserInfo{ Name: data.Login, Login: data.Login, 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/core/components/ScrollBar/ScrollBar.tsx b/public/app/core/components/ScrollBar/ScrollBar.tsx index 49a200b0f3b..7d9e015df94 100644 --- a/public/app/core/components/ScrollBar/ScrollBar.tsx +++ b/public/app/core/components/ScrollBar/ScrollBar.tsx @@ -7,7 +7,6 @@ export interface Props { } export default class ScrollBar extends React.Component { - private container: any; private ps: PerfectScrollbar; @@ -16,7 +15,9 @@ export default class ScrollBar extends React.Component { } componentDidMount() { - this.ps = new PerfectScrollbar(this.container); + this.ps = new PerfectScrollbar(this.container, { + wheelPropagation: true, + }); } componentDidUpdate() { diff --git a/public/app/core/components/form_dropdown/form_dropdown.ts b/public/app/core/components/form_dropdown/form_dropdown.ts index cf9c6b5efe0..7ac55e54cf1 100644 --- a/public/app/core/components/form_dropdown/form_dropdown.ts +++ b/public/app/core/components/form_dropdown/form_dropdown.ts @@ -34,6 +34,7 @@ export class FormDropdownCtrl { lookupText: boolean; placeholder: any; startOpen: any; + debounce: number; /** @ngInject **/ constructor(private $scope, $element, private $sce, private templateSrv, private $q) { @@ -72,6 +73,10 @@ export class FormDropdownCtrl { this.source(this.query, this.process.bind(this)); }; + if (this.debounce) { + typeahead.lookup = _.debounce(typeahead.lookup, 500, { leading: true }); + } + this.linkElement.keydown(evt => { // trigger typeahead on down arrow or enter key if (evt.keyCode === 40 || evt.keyCode === 13) { @@ -263,6 +268,7 @@ export function formDropdownDirective() { lookupText: '@', placeholder: '@', startOpen: '@', + debounce: '@', }, }; } diff --git a/public/app/core/components/help/help.ts b/public/app/core/components/help/help.ts index 0676b4dae99..c2f6f213dd3 100644 --- a/public/app/core/components/help/help.ts +++ b/public/app/core/components/help/help.ts @@ -19,7 +19,6 @@ export class HelpCtrl { ], Dashboard: [ { keys: ['mod+s'], description: 'Save dashboard' }, - { keys: ['mod+h'], description: 'Hide row controls' }, { keys: ['d', 'r'], description: 'Refresh all panels' }, { keys: ['d', 's'], description: 'Dashboard settings' }, { keys: ['d', 'v'], description: 'Toggle in-active / view mode' }, diff --git a/public/app/core/components/org_switcher.ts b/public/app/core/components/org_switcher.ts index d6efeb51779..1816e11af49 100644 --- a/public/app/core/components/org_switcher.ts +++ b/public/app/core/components/org_switcher.ts @@ -1,5 +1,6 @@ import coreModule from 'app/core/core_module'; import { contextSrv } from 'app/core/services/context_srv'; +import config from 'app/core/config'; const template = ` diff --git a/public/app/features/panel/panel_directive.ts b/public/app/features/panel/panel_directive.ts index 01730e2fede..dec7868a553 100644 --- a/public/app/features/panel/panel_directive.ts +++ b/public/app/features/panel/panel_directive.ts @@ -100,7 +100,9 @@ module.directive('grafanaPanel', function($rootScope, $document, $timeout) { // update scrollbar after mounting ctrl.events.on('component-did-mount', () => { if (ctrl.__proto__.constructor.scrollable) { - panelScrollbar = new PerfectScrollbar(panelContent[0]); + panelScrollbar = new PerfectScrollbar(panelContent[0], { + wheelPropagation: true, + }); } }); diff --git a/public/app/features/panel/solo_panel_ctrl.ts b/public/app/features/panel/solo_panel_ctrl.ts index 323a88ddaee..2c7698db08e 100644 --- a/public/app/features/panel/solo_panel_ctrl.ts +++ b/public/app/features/panel/solo_panel_ctrl.ts @@ -9,7 +9,7 @@ export class SoloPanelCtrl { $scope.init = function() { contextSrv.sidemenu = false; - appEvents.emit('toggle-sidemenu'); + appEvents.emit('toggle-sidemenu-hidden'); var params = $location.search(); panelId = parseInt(params.panelId); 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/features/plugins/partials/ds_list.html b/public/app/features/plugins/partials/ds_list.html index b247c59ab66..fd537fc47d4 100644 --- a/public/app/features/plugins/partials/ds_list.html +++ b/public/app/features/plugins/partials/ds_list.html @@ -52,7 +52,7 @@