Преглед на файлове

Merge branch 'master' into provisioning

Leonard Gram преди 7 години
родител
ревизия
9a7eb5c327

+ 17 - 2
README.md

@@ -80,8 +80,11 @@ In your custom.ini uncomment (remove the leading `;`) sign. And set `app_mode =
 
 
 ### Running tests
 ### Running tests
 
 
-- You can run backend Golang tests using "go test ./pkg/...".
-- 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)
 Writing & watching frontend tests (we have two test runners)
 
 
@@ -92,6 +95,18 @@ Writing & watching frontend tests (we have two test runners)
   - Start watcher: `npm run karma`
   - Start watcher: `npm run karma`
   - Karma+Mocha runs all files that end with the name "_specs.ts".
   - Karma+Mocha runs all files that end with the name "_specs.ts".
 
 
+#### Backend
+```bash
+# Run Golang tests using sqlite3 as database (default)
+go test ./pkg/... 
+
+# Run Golang tests using mysql as database - convenient to use /docker/blocks/mysql_tests
+GRAFANA_TEST_DB=mysql go test ./pkg/... 
+
+# Run Golang tests using postgres as database - convenient to use /docker/blocks/postgres_tests
+GRAFANA_TEST_DB=postgres go test ./pkg/... 
+```
+
 ## Contribute
 ## Contribute
 
 
 If you have any idea for an improvement or found a bug, do not hesitate to open an issue.
 If you have any idea for an improvement or found a bug, do not hesitate to open an issue.

+ 97 - 39
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"]
 keywords = ["grafana", "installation", "documentation"]
 type = "docs"
 type = "docs"
 aliases = ["v1.1", "guides/reference/admin"]
 aliases = ["v1.1", "guides/reference/admin"]
 +++
 +++
 
 
-# Welcome to the Grafana Documentation
+# Grafana Documentation
 
 
-Grafana is an open source metric analytics & visualization suite. It is most commonly used for
-visualizing time series data for infrastructure and application analytics but many use it in
-other domains including industrial sensors, home automation, weather, and process control.
+<h2>Installing Grafana</h2>
+<div class="nav-cards">
+    <a href="{{< relref "installation/debian.md" >}}" class="nav-cards__item nav-cards__item--install">
+        <div class="nav-cards__icon fa fa-linux">
+        </div>
+        <h5>Installing on Linux</h5>
+    </a>
+    <a href="{{< relref "installation/mac.md" >}}" class="nav-cards__item nav-cards__item--install">
+        <div class="nav-cards__icon fa fa-apple">
+        </div>
+        <h5>Installing on Mac OS X</h5>
+    </a>
+      <a href="{{< relref "installation/windows.md" >}}" class="nav-cards__item nav-cards__item--install">
+        <div class="nav-cards__icon fa fa-windows">
+        </div>
+        <h5>Installing on Windows</h5>
+    </a>
+    <a href="https://grafana.com/cloud/grafana" class="nav-cards__item nav-cards__item--install">
+        <div class="nav-cards__icon fa fa-cloud">
+        </div>
+        <h5>Grafana Cloud</h5>
+    </a>
+    <a href="https://grafana.com/grafana/download" class="nav-cards__item nav-cards__item--install">
+        <div class="nav-cards__icon fa fa-moon-o">
+        </div>
+        <h5>Nightly Builds</h5>
+    </a>
+    <div class="nav-cards__item nav-cards__item--install">
+        <h5>For other platforms Read the <a href="{{< relref "project/building_from_source.md" >}}">build from source</a>
+        instructions for more information.</h5>
+    </div>
+</div>
 
 
-## Installing Grafana
-- [Installing on Debian / Ubuntu](installation/debian)
-- [Installing on RPM-based Linux (CentOS, Fedora, OpenSuse, RedHat)](installation/rpm)
-- [Installing on Mac OS X](installation/mac)
-- [Installing on Windows](installation/windows)
-- [Installing on Docker](installation/docker)
-- [Installing using Provisioning (Chef, Puppet, Salt, Ansible, etc)](administration/provisioning#configuration-management-tools)
-- [Nightly Builds](https://grafana.com/grafana/download)
+<h2>Guides</h2>
 
 
-For other platforms Read the [build from source]({{< relref "project/building_from_source.md" >}})
-instructions for more information.
+<div class="nav-cards">
+    <a href="https://grafana.com/grafana" class="nav-cards__item nav-cards__item--guide">
+        <h4>What is Grafana?</h4>
+        <p>Grafana feature highlights.</p>
+    </a>
+    <a href="{{< relref "installation/configuration.md" >}}" class="nav-cards__item nav-cards__item--guide">
+        <h4>Configure Grafana</h4>
+        <p>Article on all the Grafana configuration and setup options.</p>
+    </a>
+    <a href="{{< relref "guides/getting_started.md" >}}" class="nav-cards__item nav-cards__item--guide">
+        <h4>Getting Started</h4>
+        <p>A guide that walks you through the basics of using Grafana</p>
+    </a>
+    <a href="{{< relref "administration/provisioning.md" >}}" class="nav-cards__item nav-cards__item--guide">
+        <h4>Provisioning</h4>
+        <p>A guide to help you automate your Grafana setup & configuration.</p>
+    </a>
+    <a href="{{< relref "guides/whats-new-in-v5.md" >}}" class="nav-cards__item nav-cards__item--guide">
+        <h4>What's new in v5.0</h4>
+        <p>Article on all the new cool features and enhancements in v5.0</p>
+    </a>
+    <a href="{{< relref "tutorials/screencasts.md" >}}" class="nav-cards__item nav-cards__item--guide">
+        <h4>Screencasts</h4>
+        <p>Video tutorials & guides</p>
+    </a>
+</div>
 
 
-## Configuring Grafana
-
-The back-end web server has a number of configuration options. Go the
-[Configuration]({{< relref "installation/configuration.md" >}}) page for details on all
-those options.
-
-
-## Getting Started
-
-- [Getting Started]({{< relref "guides/getting_started.md" >}})
-- [Basic Concepts]({{< relref "guides/basic_concepts.md" >}})
-- [Screencasts]({{< relref "tutorials/screencasts.md" >}})
-
-## Data Source Guides
-
-- [Graphite]({{< relref "features/datasources/graphite.md" >}})
-- [Elasticsearch]({{< relref "features/datasources/elasticsearch.md" >}})
-- [InfluxDB]({{< relref "features/datasources/influxdb.md" >}})
-- [Prometheus]({{< relref "features/datasources/prometheus.md" >}})
-- [OpenTSDB]({{< relref "features/datasources/opentsdb.md" >}})
-- [MySQL]({{< relref "features/datasources/mysql.md" >}})
-- [Postgres]({{< relref "features/datasources/postgres.md" >}})
-- [Cloudwatch]({{< relref "features/datasources/cloudwatch.md" >}})
+<h2>Data Source Guides</h2>
+<div class="nav-cards">
+    <a href="{{< relref "features/datasources/graphite.md" >}}" class="nav-cards__item nav-cards__item--ds">
+      <img src="/img/docs/logos/icon_graphite.svg" >
+      <h5>Graphite</h5>
+    </a>
+    <a href="{{< relref "features/datasources/elasticsearch.md" >}}" class="nav-cards__item nav-cards__item--ds">
+      <img src="/img/docs/logos/icon_elasticsearch.svg" >
+      <h5>Elasticsearch</h5>
+    </a>
+    <a href="{{< relref "features/datasources/influxdb.md" >}}" class="nav-cards__item nav-cards__item--ds">
+      <img src="/img/docs/logos/icon_influxdb.svg" >
+      <h5>InfluxDB</h5>
+    </a>
+    <a href="{{< relref "features/datasources/prometheus.md" >}}" class="nav-cards__item nav-cards__item--ds">
+      <img src="/img/docs/logos/icon_prometheus.svg" >
+      <h5>Prometheus</h5>
+    </a>
+    <a href="{{< relref "features/datasources/opentsdb.md" >}}" class="nav-cards__item nav-cards__item--ds">
+      <img src="/img/docs/logos/icon_opentsdb.png" >
+      <h5>OpenTSDB</h5>
+    </a>
+    <a href="{{< relref "features/datasources/mysql.md" >}}" class="nav-cards__item nav-cards__item--ds">
+      <img src="/img/docs/logos/icon_mysql.png" >
+      <h5>MySQL</h5>
+    </a>
+    <a href="{{< relref "features/datasources/postgres.md" >}}" class="nav-cards__item nav-cards__item--ds">
+      <img src="/img/docs/logos/icon_postgres.svg" >
+      <h5>Postgres</h5>
+    </a>
+    <a href="{{< relref "features/datasources/cloudwatch.md" >}}" class="nav-cards__item nav-cards__item--ds">
+      <img src="/img/docs/logos/icon_cloudwatch.svg">
+      <h5>Cloudwatch</h5>
+    </a>
+</div>

+ 7 - 7
pkg/api/api.go

@@ -150,13 +150,13 @@ func (hs *HttpServer) registerRoutes() {
 		apiRoute.Group("/teams", func(teamsRoute RouteRegister) {
 		apiRoute.Group("/teams", func(teamsRoute RouteRegister) {
 			teamsRoute.Get("/:teamId", wrap(GetTeamById))
 			teamsRoute.Get("/:teamId", wrap(GetTeamById))
 			teamsRoute.Get("/search", wrap(SearchTeams))
 			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.
 		// org information available to all users.
 		apiRoute.Group("/org", func(orgRoute RouteRegister) {
 		apiRoute.Group("/org", func(orgRoute RouteRegister) {

+ 15 - 0
pkg/api/dashboard_acl.go

@@ -13,6 +13,11 @@ import (
 func GetDashboardAclList(c *middleware.Context) Response {
 func GetDashboardAclList(c *middleware.Context) Response {
 	dashId := c.ParamsInt64(":dashboardId")
 	dashId := c.ParamsInt64(":dashboardId")
 
 
+	_, rsp := getDashboardHelper(c.OrgId, "", dashId, "")
+	if rsp != nil {
+		return rsp
+	}
+
 	guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
 	guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
 
 
 	if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin {
 	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 {
 func UpdateDashboardAcl(c *middleware.Context, apiCmd dtos.UpdateDashboardAclCommand) Response {
 	dashId := c.ParamsInt64(":dashboardId")
 	dashId := c.ParamsInt64(":dashboardId")
 
 
+	_, rsp := getDashboardHelper(c.OrgId, "", dashId, "")
+	if rsp != nil {
+		return rsp
+	}
+
 	guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
 	guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
 	if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin {
 	if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin {
 		return dashboardGuardianResponse(err)
 		return dashboardGuardianResponse(err)
@@ -79,6 +89,11 @@ func DeleteDashboardAcl(c *middleware.Context) Response {
 	dashId := c.ParamsInt64(":dashboardId")
 	dashId := c.ParamsInt64(":dashboardId")
 	aclId := c.ParamsInt64(":aclId")
 	aclId := c.ParamsInt64(":aclId")
 
 
+	_, rsp := getDashboardHelper(c.OrgId, "", dashId, "")
+	if rsp != nil {
+		return rsp
+	}
+
 	guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
 	guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
 	if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin {
 	if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin {
 		return dashboardGuardianResponse(err)
 		return dashboardGuardianResponse(err)

+ 42 - 0
pkg/api/dashboard_acl_test.go

@@ -23,6 +23,14 @@ func TestDashboardAclApiEndpoint(t *testing.T) {
 		}
 		}
 		dtoRes := transformDashboardAclsToDTOs(mockResult)
 		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 {
 		bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
 			query.Result = dtoRes
 			query.Result = dtoRes
 			return nil
 			return nil
@@ -60,6 +68,40 @@ func TestDashboardAclApiEndpoint(t *testing.T) {
 					So(respJSON.GetIndex(0).Get("permission").MustInt(), ShouldEqual, m.PERMISSION_VIEW)
 					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() {
 		Convey("When user is org editor and has admin permission in the ACL", func() {

+ 11 - 7
pkg/api/org_users.go

@@ -46,26 +46,30 @@ func addOrgUserHelper(cmd m.AddOrgUserCommand) Response {
 
 
 // GET /api/org/users
 // GET /api/org/users
 func GetOrgUsersForCurrentOrg(c *middleware.Context) Response {
 func GetOrgUsersForCurrentOrg(c *middleware.Context) Response {
-	return getOrgUsersHelper(c.OrgId)
+	return getOrgUsersHelper(c.OrgId, c.Params("query"), c.ParamsInt("limit"))
 }
 }
 
 
 // GET /api/orgs/:orgId/users
 // GET /api/orgs/:orgId/users
 func GetOrgUsers(c *middleware.Context) Response {
 func GetOrgUsers(c *middleware.Context) Response {
-	return getOrgUsersHelper(c.ParamsInt64(":orgId"))
+	return getOrgUsersHelper(c.ParamsInt64(":orgId"), "", 0)
 }
 }
 
 
-func getOrgUsersHelper(orgId int64) Response {
-	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)
 		return ApiError(500, "Failed to get account user", err)
 	}
 	}
 
 
-	for _, user := range query.Result {
+	for _, user := range q.Result {
 		user.AvatarUrl = dtos.GetGravatarUrl(user.Email)
 		user.AvatarUrl = dtos.GetGravatarUrl(user.Email)
 	}
 	}
 
 
-	return Json(200, query.Result)
+	return Json(200, q.Result)
 }
 }
 
 
 // PATCH /api/org/users/:userId
 // PATCH /api/org/users/:userId

+ 4 - 3
pkg/api/team.go

@@ -26,6 +26,7 @@ func CreateTeam(c *middleware.Context, cmd m.CreateTeamCommand) Response {
 
 
 // PUT /api/teams/:teamId
 // PUT /api/teams/:teamId
 func UpdateTeam(c *middleware.Context, cmd m.UpdateTeamCommand) Response {
 func UpdateTeam(c *middleware.Context, cmd m.UpdateTeamCommand) Response {
+	cmd.OrgId = c.OrgId
 	cmd.Id = c.ParamsInt64(":teamId")
 	cmd.Id = c.ParamsInt64(":teamId")
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
 		if err == m.ErrTeamNameTaken {
 		if err == m.ErrTeamNameTaken {
@@ -39,7 +40,7 @@ func UpdateTeam(c *middleware.Context, cmd m.UpdateTeamCommand) Response {
 
 
 // DELETE /api/teams/:teamId
 // DELETE /api/teams/:teamId
 func DeleteTeamById(c *middleware.Context) Response {
 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 {
 		if err == m.ErrTeamNotFound {
 			return ApiError(404, "Failed to delete Team. ID not found", nil)
 			return ApiError(404, "Failed to delete Team. ID not found", nil)
 		}
 		}
@@ -60,11 +61,11 @@ func SearchTeams(c *middleware.Context) Response {
 	}
 	}
 
 
 	query := m.SearchTeamsQuery{
 	query := m.SearchTeamsQuery{
+		OrgId: c.OrgId,
 		Query: c.Query("query"),
 		Query: c.Query("query"),
 		Name:  c.Query("name"),
 		Name:  c.Query("name"),
 		Page:  page,
 		Page:  page,
 		Limit: perPage,
 		Limit: perPage,
-		OrgId: c.OrgId,
 	}
 	}
 
 
 	if err := bus.Dispatch(&query); err != nil {
 	if err := bus.Dispatch(&query); err != nil {
@@ -83,7 +84,7 @@ func SearchTeams(c *middleware.Context) Response {
 
 
 // GET /api/teams/:teamId
 // GET /api/teams/:teamId
 func GetTeamById(c *middleware.Context) Response {
 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 := bus.Dispatch(&query); err != nil {
 		if err == m.ErrTeamNotFound {
 		if err == m.ErrTeamNotFound {

+ 2 - 2
pkg/api/team_members.go

@@ -10,7 +10,7 @@ import (
 
 
 // GET /api/teams/:teamId/members
 // GET /api/teams/:teamId/members
 func GetTeamMembers(c *middleware.Context) Response {
 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 {
 	if err := bus.Dispatch(&query); err != nil {
 		return ApiError(500, "Failed to get Team Members", err)
 		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
 // DELETE /api/teams/:teamId/members/:userId
 func RemoveTeamMember(c *middleware.Context) Response {
 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 ApiError(500, "Failed to remove Member from Team", err)
 	}
 	}
 	return ApiSuccess("Team Member removed")
 	return ApiSuccess("Team Member removed")

+ 4 - 1
pkg/models/org_user.go

@@ -95,7 +95,10 @@ type UpdateOrgUserCommand struct {
 // QUERIES
 // QUERIES
 
 
 type GetOrgUsersQuery struct {
 type GetOrgUsersQuery struct {
-	OrgId  int64
+	OrgId int64
+	Query string
+	Limit int
+
 	Result []*OrgUserDTO
 	Result []*OrgUserDTO
 }
 }
 
 

+ 5 - 1
pkg/models/team.go

@@ -37,18 +37,22 @@ type UpdateTeamCommand struct {
 	Id    int64
 	Id    int64
 	Name  string
 	Name  string
 	Email string
 	Email string
+	OrgId int64 `json:"-"`
 }
 }
 
 
 type DeleteTeamCommand struct {
 type DeleteTeamCommand struct {
-	Id int64
+	OrgId int64
+	Id    int64
 }
 }
 
 
 type GetTeamByIdQuery struct {
 type GetTeamByIdQuery struct {
+	OrgId  int64
 	Id     int64
 	Id     int64
 	Result *Team
 	Result *Team
 }
 }
 
 
 type GetTeamsByUserQuery struct {
 type GetTeamsByUserQuery struct {
+	OrgId  int64
 	UserId int64   `json:"userId"`
 	UserId int64   `json:"userId"`
 	Result []*Team `json:"teams"`
 	Result []*Team `json:"teams"`
 }
 }

+ 2 - 0
pkg/models/team_member.go

@@ -31,6 +31,7 @@ type AddTeamMemberCommand struct {
 }
 }
 
 
 type RemoveTeamMemberCommand struct {
 type RemoveTeamMemberCommand struct {
+	OrgId  int64 `json:"-"`
 	UserId int64
 	UserId int64
 	TeamId int64
 	TeamId int64
 }
 }
@@ -39,6 +40,7 @@ type RemoveTeamMemberCommand struct {
 // QUERIES
 // QUERIES
 
 
 type GetTeamMembersQuery struct {
 type GetTeamMembersQuery struct {
+	OrgId  int64
 	TeamId int64
 	TeamId int64
 	Result []*TeamMemberDTO
 	Result []*TeamMemberDTO
 }
 }

+ 1 - 1
pkg/services/guardian/guardian.go

@@ -160,7 +160,7 @@ func (g *DashboardGuardian) getTeams() ([]*m.Team, error) {
 		return g.groups, nil
 		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)
 	err := bus.Dispatch(&query)
 
 
 	g.groups = query.Result
 	g.groups = query.Result

+ 28 - 3
pkg/services/sqlstore/datasource_test.go

@@ -1,6 +1,8 @@
 package sqlstore
 package sqlstore
 
 
 import (
 import (
+	"os"
+	"strings"
 	"testing"
 	"testing"
 
 
 	"github.com/go-xorm/xorm"
 	"github.com/go-xorm/xorm"
@@ -11,10 +13,33 @@ import (
 	"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
 	"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
 )
 )
 
 
+var (
+	dbSqlite   = "sqlite"
+	dbMySql    = "mysql"
+	dbPostgres = "postgres"
+)
+
 func InitTestDB(t *testing.T) *xorm.Engine {
 func InitTestDB(t *testing.T) *xorm.Engine {
-	x, err := xorm.NewEngine(sqlutil.TestDB_Sqlite3.DriverName, sqlutil.TestDB_Sqlite3.ConnStr)
-	//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()
 	// x.ShowSQL()
 
 

+ 25 - 0
pkg/services/sqlstore/org_test.go

@@ -123,6 +123,31 @@ func TestAccountDataAccess(t *testing.T) {
 					So(query.Result[0].Role, ShouldEqual, "Admin")
 					So(query.Result[0].Role, ShouldEqual, "Admin")
 				})
 				})
 
 
+				Convey("Can get organization users with query", func() {
+					query := m.GetOrgUsersQuery{
+						OrgId: ac1.OrgId,
+						Query: "ac1",
+					}
+					err := GetOrgUsers(&query)
+
+					So(err, ShouldBeNil)
+					So(len(query.Result), ShouldEqual, 1)
+					So(query.Result[0].Email, ShouldEqual, ac1.Email)
+				})
+
+				Convey("Can get organization users with query and limit", func() {
+					query := m.GetOrgUsersQuery{
+						OrgId: ac1.OrgId,
+						Query: "ac",
+						Limit: 1,
+					}
+					err := GetOrgUsers(&query)
+
+					So(err, ShouldBeNil)
+					So(len(query.Result), ShouldEqual, 1)
+					So(query.Result[0].Email, ShouldEqual, ac1.Email)
+				})
+
 				Convey("Can set using org", func() {
 				Convey("Can set using org", func() {
 					cmd := m.SetUsingOrgCommand{UserId: ac2.Id, OrgId: ac1.Id}
 					cmd := m.SetUsingOrgCommand{UserId: ac2.Id, OrgId: ac1.Id}
 					err := SetUsingOrg(&cmd)
 					err := SetUsingOrg(&cmd)

+ 23 - 1
pkg/services/sqlstore/org_users.go

@@ -2,6 +2,7 @@ package sqlstore
 
 
 import (
 import (
 	"fmt"
 	"fmt"
+	"strings"
 	"time"
 	"time"
 
 
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
@@ -69,9 +70,30 @@ func UpdateOrgUser(cmd *m.UpdateOrgUserCommand) error {
 
 
 func GetOrgUsers(query *m.GetOrgUsersQuery) error {
 func GetOrgUsers(query *m.GetOrgUsersQuery) error {
 	query.Result = make([]*m.OrgUserDTO, 0)
 	query.Result = make([]*m.OrgUserDTO, 0)
+
 	sess := x.Table("org_user")
 	sess := x.Table("org_user")
 	sess.Join("INNER", "user", fmt.Sprintf("org_user.user_id=%s.id", x.Dialect().Quote("user")))
 	sess.Join("INNER", "user", fmt.Sprintf("org_user.user_id=%s.id", x.Dialect().Quote("user")))
-	sess.Where("org_user.org_id=?", query.OrgId)
+
+	whereConditions := make([]string, 0)
+	whereParams := make([]interface{}, 0)
+
+	whereConditions = append(whereConditions, "org_user.org_id = ?")
+	whereParams = append(whereParams, query.OrgId)
+
+	if query.Query != "" {
+		queryWithWildcards := "%" + query.Query + "%"
+		whereConditions = append(whereConditions, "(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.Cols("org_user.org_id", "org_user.user_id", "user.email", "user.login", "org_user.role", "user.last_seen_at")
 	sess.Asc("user.email", "user.login")
 	sess.Asc("user.email", "user.login")
 
 

+ 17 - 16
pkg/services/sqlstore/team.go

@@ -25,7 +25,7 @@ func init() {
 func CreateTeam(cmd *m.CreateTeamCommand) error {
 func CreateTeam(cmd *m.CreateTeamCommand) error {
 	return inTransaction(func(sess *DBSession) 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
 			return err
 		} else if isNameTaken {
 		} else if isNameTaken {
 			return m.ErrTeamNameTaken
 			return m.ErrTeamNameTaken
@@ -50,7 +50,7 @@ func CreateTeam(cmd *m.CreateTeamCommand) error {
 func UpdateTeam(cmd *m.UpdateTeamCommand) error {
 func UpdateTeam(cmd *m.UpdateTeamCommand) error {
 	return inTransaction(func(sess *DBSession) 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
 			return err
 		} else if isNameTaken {
 		} else if isNameTaken {
 			return m.ErrTeamNameTaken
 			return m.ErrTeamNameTaken
@@ -80,20 +80,20 @@ func UpdateTeam(cmd *m.UpdateTeamCommand) error {
 
 
 func DeleteTeam(cmd *m.DeleteTeamCommand) error {
 func DeleteTeam(cmd *m.DeleteTeamCommand) error {
 	return inTransaction(func(sess *DBSession) 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
 			return err
 		} else if len(res) != 1 {
 		} else if len(res) != 1 {
 			return m.ErrTeamNotFound
 			return m.ErrTeamNotFound
 		}
 		}
 
 
 		deletes := []string{
 		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 {
 		for _, sql := range deletes {
-			_, err := sess.Exec(sql, cmd.Id)
+			_, err := sess.Exec(sql, cmd.OrgId, cmd.Id)
 			if err != nil {
 			if err != nil {
 				return err
 				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
 	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 {
 	if err != nil {
 		return false, nil
 		return false, nil
@@ -128,6 +128,7 @@ func SearchTeams(query *m.SearchTeamsQuery) error {
 
 
 	sql.WriteString(`select
 	sql.WriteString(`select
 		team.id as id,
 		team.id as id,
+		team.org_id,
 		team.name as name,
 		team.name as name,
 		team.email as email,
 		team.email as email,
 		(select count(*) from team_member where team_member.team_id = team.id) as member_count
 		(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 {
 func GetTeamById(query *m.GetTeamByIdQuery) error {
 	var team m.Team
 	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 {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -194,7 +195,7 @@ func GetTeamsByUser(query *m.GetTeamsByUserQuery) error {
 
 
 	sess := x.Table("team")
 	sess := x.Table("team")
 	sess.Join("INNER", "team_member", "team.id=team_member.team_id")
 	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)
 	err := sess.Find(&query.Result)
 	if err != nil {
 	if err != nil {
@@ -206,13 +207,13 @@ func GetTeamsByUser(query *m.GetTeamsByUserQuery) error {
 
 
 func AddTeamMember(cmd *m.AddTeamMemberCommand) error {
 func AddTeamMember(cmd *m.AddTeamMemberCommand) error {
 	return inTransaction(func(sess *DBSession) 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
 			return err
 		} else if len(res) == 1 {
 		} else if len(res) == 1 {
 			return m.ErrTeamMemberAlreadyAdded
 			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
 			return err
 		} else if len(res) != 1 {
 		} else if len(res) != 1 {
 			return m.ErrTeamNotFound
 			return m.ErrTeamNotFound
@@ -233,8 +234,8 @@ func AddTeamMember(cmd *m.AddTeamMemberCommand) error {
 
 
 func RemoveTeamMember(cmd *m.RemoveTeamMemberCommand) error {
 func RemoveTeamMember(cmd *m.RemoveTeamMemberCommand) error {
 	return inTransaction(func(sess *DBSession) 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 {
 		if err != nil {
 			return err
 			return err
 		}
 		}
@@ -247,7 +248,7 @@ func GetTeamMembers(query *m.GetTeamMembersQuery) error {
 	query.Result = make([]*m.TeamMemberDTO, 0)
 	query.Result = make([]*m.TeamMemberDTO, 0)
 	sess := x.Table("team_member")
 	sess := x.Table("team_member")
 	sess.Join("INNER", "user", fmt.Sprintf("team_member.user_id=%s.id", x.Dialect().Quote("user")))
 	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.Cols("user.org_id", "team_member.team_id", "team_member.user_id", "user.email", "user.login")
 	sess.Asc("user.login", "user.email")
 	sess.Asc("user.login", "user.email")
 
 

+ 19 - 16
pkg/services/sqlstore/team_test.go

@@ -27,8 +27,9 @@ func TestTeamCommandsAndQueries(t *testing.T) {
 				userIds = append(userIds, userCmd.Result.Id)
 				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)
 			err := CreateTeam(&group1)
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
@@ -36,7 +37,7 @@ func TestTeamCommandsAndQueries(t *testing.T) {
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
 
 
 			Convey("Should be able to create teams and add users", func() {
 			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)
 				err = SearchTeams(query)
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)
 				So(query.Page, ShouldEqual, 1)
 				So(query.Page, ShouldEqual, 1)
@@ -44,25 +45,27 @@ func TestTeamCommandsAndQueries(t *testing.T) {
 				team1 := query.Result.Teams[0]
 				team1 := query.Result.Teams[0]
 				So(team1.Name, ShouldEqual, "group1 name")
 				So(team1.Name, ShouldEqual, "group1 name")
 				So(team1.Email, ShouldEqual, "test1@test.com")
 				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)
 				So(err, ShouldBeNil)
 
 
-				q1 := &m.GetTeamMembersQuery{TeamId: team1.Id}
+				q1 := &m.GetTeamMembersQuery{OrgId: testOrgId, TeamId: team1.Id}
 				err = GetTeamMembers(q1)
 				err = GetTeamMembers(q1)
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)
 				So(q1.Result[0].TeamId, ShouldEqual, team1.Id)
 				So(q1.Result[0].TeamId, ShouldEqual, team1.Id)
 				So(q1.Result[0].Login, ShouldEqual, "loginuser0")
 				So(q1.Result[0].Login, ShouldEqual, "loginuser0")
+				So(q1.Result[0].OrgId, ShouldEqual, testOrgId)
 			})
 			})
 
 
 			Convey("Should be able to search for teams", func() {
 			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)
 				err = SearchTeams(query)
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)
 				So(len(query.Result.Teams), ShouldEqual, 2)
 				So(len(query.Result.Teams), ShouldEqual, 2)
 				So(query.Result.TotalCount, ShouldEqual, 2)
 				So(query.Result.TotalCount, ShouldEqual, 2)
 
 
-				query2 := &m.SearchTeamsQuery{Query: ""}
+				query2 := &m.SearchTeamsQuery{OrgId: testOrgId, Query: ""}
 				err = SearchTeams(query2)
 				err = SearchTeams(query2)
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)
 				So(len(query2.Result.Teams), ShouldEqual, 2)
 				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() {
 			Convey("Should be able to return all teams a user is member of", func() {
 				groupId := group2.Result.Id
 				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)
 				err = GetTeamsByUser(query)
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)
 				So(len(query.Result), ShouldEqual, 1)
 				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() {
 			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)
 				So(err, ShouldBeNil)
 
 
 				q1 := &m.GetTeamMembersQuery{TeamId: group1.Result.Id}
 				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() {
 			Convey("Should be able to remove a group with users and permissions", func() {
 				groupId := group2.Result.Id
 				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)
 				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)
 				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)
 				So(err, ShouldBeNil)
 
 
-				query := &m.GetTeamByIdQuery{Id: groupId}
+				query := &m.GetTeamByIdQuery{OrgId: testOrgId, Id: groupId}
 				err = GetTeamById(query)
 				err = GetTeamById(query)
 				So(err, ShouldEqual, m.ErrTeamNotFound)
 				So(err, ShouldEqual, m.ErrTeamNotFound)
 
 
-				permQuery := &m.GetDashboardAclInfoListQuery{DashboardId: 1, OrgId: 1}
+				permQuery := &m.GetDashboardAclInfoListQuery{DashboardId: 1, OrgId: testOrgId}
 				err = GetDashboardAclInfoList(permQuery)
 				err = GetDashboardAclInfoList(permQuery)
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)
 
 

+ 1 - 1
pkg/social/github_oauth.go

@@ -210,7 +210,7 @@ func (s *SocialGithub) UserInfo(client *http.Client, token *oauth2.Token) (*Basi
 	if err != nil {
 	if err != nil {
 		return nil, fmt.Errorf("Error getting user info: %s", err)
 		return nil, fmt.Errorf("Error getting user info: %s", err)
 	}
 	}
-
+	data.OrganizationsUrl = s.apiUrl + "/user/orgs"
 	userInfo := &BasicUserInfo{
 	userInfo := &BasicUserInfo{
 		Name:  data.Login,
 		Name:  data.Login,
 		Login: data.Login,
 		Login: data.Login,

+ 4 - 4
public/app/core/components/Picker/UserPicker.tsx

@@ -31,7 +31,7 @@ class UserPicker extends Component<IProps, any> {
 
 
     this.debouncedSearch = debounce(this.search, 300, {
     this.debouncedSearch = debounce(this.search, 300, {
       leading: true,
       leading: true,
-      trailing: false,
+      trailing: true,
     });
     });
   }
   }
 
 
@@ -39,10 +39,10 @@ class UserPicker extends Component<IProps, any> {
     const { toggleLoading, backendSrv } = this.props;
     const { toggleLoading, backendSrv } = this.props;
 
 
     toggleLoading(true);
     toggleLoading(true);
-    return backendSrv.get(`/api/users/search?perpage=10&page=1&query=${query}`).then(result => {
-      const users = result.users.map(user => {
+    return backendSrv.get(`/api/org/users?query=${query}&limit=10`).then(result => {
+      const users = result.map(user => {
         return {
         return {
-          id: user.id,
+          id: user.userId,
           label: `${user.login} - ${user.email}`,
           label: `${user.login} - ${user.email}`,
           avatarUrl: user.avatarUrl,
           avatarUrl: user.avatarUrl,
           login: user.login,
           login: user.login,

+ 6 - 0
public/app/core/components/form_dropdown/form_dropdown.ts

@@ -34,6 +34,7 @@ export class FormDropdownCtrl {
   lookupText: boolean;
   lookupText: boolean;
   placeholder: any;
   placeholder: any;
   startOpen: any;
   startOpen: any;
+  debounce: number;
 
 
   /** @ngInject **/
   /** @ngInject **/
   constructor(private $scope, $element, private $sce, private templateSrv, private $q) {
   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));
       this.source(this.query, this.process.bind(this));
     };
     };
 
 
+    if (this.debounce) {
+      typeahead.lookup = _.debounce(typeahead.lookup, 500, { leading: true });
+    }
+
     this.linkElement.keydown(evt => {
     this.linkElement.keydown(evt => {
       // trigger typeahead on down arrow or enter key
       // trigger typeahead on down arrow or enter key
       if (evt.keyCode === 40 || evt.keyCode === 13) {
       if (evt.keyCode === 40 || evt.keyCode === 13) {
@@ -263,6 +268,7 @@ export function formDropdownDirective() {
       lookupText: '@',
       lookupText: '@',
       placeholder: '@',
       placeholder: '@',
       startOpen: '@',
       startOpen: '@',
+      debounce: '@',
     },
     },
   };
   };
 }
 }

+ 6 - 0
public/app/core/components/query_part/query_part_editor.ts

@@ -23,11 +23,13 @@ export function queryPartEditorDirective($compile, templateSrv) {
     scope: {
     scope: {
       part: '=',
       part: '=',
       handleEvent: '&',
       handleEvent: '&',
+      debounce: '@',
     },
     },
     link: function postLink($scope, elem) {
     link: function postLink($scope, elem) {
       var part = $scope.part;
       var part = $scope.part;
       var partDef = part.def;
       var partDef = part.def;
       var $paramsContainer = elem.find('.query-part-parameters');
       var $paramsContainer = elem.find('.query-part-parameters');
+      var debounceLookup = $scope.debounce;
 
 
       $scope.partActions = [];
       $scope.partActions = [];
 
 
@@ -128,6 +130,10 @@ export function queryPartEditorDirective($compile, templateSrv) {
           var items = this.source(this.query, $.proxy(this.process, this));
           var items = this.source(this.query, $.proxy(this.process, this));
           return items ? this.process(items) : items;
           return items ? this.process(items) : items;
         };
         };
+
+        if (debounceLookup) {
+          typeahead.lookup = _.debounce(typeahead.lookup, 500, { leading: true });
+        }
       }
       }
 
 
       $scope.showActionsMenu = function() {
       $scope.showActionsMenu = function() {

+ 6 - 0
public/app/core/directives/metric_segment.js

@@ -22,6 +22,7 @@ function (_, $, coreModule) {
         segment: "=",
         segment: "=",
         getOptions: "&",
         getOptions: "&",
         onChange: "&",
         onChange: "&",
+        debounce: "@",
       },
       },
       link: function($scope, elem) {
       link: function($scope, elem) {
         var $input = $(inputTemplate);
         var $input = $(inputTemplate);
@@ -30,6 +31,7 @@ function (_, $, coreModule) {
         var options = null;
         var options = null;
         var cancelBlur = null;
         var cancelBlur = null;
         var linkMode = true;
         var linkMode = true;
+        var debounceLookup = $scope.debounce;
 
 
         $input.appendTo(elem);
         $input.appendTo(elem);
         $button.appendTo(elem);
         $button.appendTo(elem);
@@ -135,6 +137,10 @@ function (_, $, coreModule) {
           return items ? this.process(items) : items;
           return items ? this.process(items) : items;
         };
         };
 
 
+        if (debounceLookup) {
+          typeahead.lookup = _.debounce(typeahead.lookup, 500, {leading: true});
+        }
+
         $button.keydown(function(evt) {
         $button.keydown(function(evt) {
           // trigger typeahead on down arrow or enter key
           // trigger typeahead on down arrow or enter key
           if (evt.keyCode === 40 || evt.keyCode === 13) {
           if (evt.keyCode === 40 || evt.keyCode === 13) {

+ 3 - 5
public/app/plugins/datasource/graphite/partials/query.editor.html

@@ -13,9 +13,9 @@
       <div ng-if="ctrl.queryModel.seriesByTagUsed" ng-repeat="tag in ctrl.queryModel.tags" class="gf-form">
       <div ng-if="ctrl.queryModel.seriesByTagUsed" ng-repeat="tag in ctrl.queryModel.tags" class="gf-form">
         <gf-form-dropdown
         <gf-form-dropdown
           model="tag.key"
           model="tag.key"
-          lookup-text="false"
           allow-custom="true"
           allow-custom="true"
           label-mode="true"
           label-mode="true"
+          debounce="true"
           placeholder="Tag key"
           placeholder="Tag key"
           css-class="query-segment-key"
           css-class="query-segment-key"
           get-options="ctrl.getTags($index, $query)"
           get-options="ctrl.getTags($index, $query)"
@@ -23,8 +23,6 @@
         />
         />
         <gf-form-dropdown
         <gf-form-dropdown
           model="tag.operator"
           model="tag.operator"
-          lookup-text="false"
-          allow-custom="false"
           label-mode="true"
           label-mode="true"
           css-class="query-segment-operator"
           css-class="query-segment-operator"
           get-options="ctrl.getTagOperators()"
           get-options="ctrl.getTagOperators()"
@@ -33,9 +31,9 @@
         />
         />
         <gf-form-dropdown
         <gf-form-dropdown
           model="tag.value"
           model="tag.value"
-          lookup-text="false"
           allow-custom="true"
           allow-custom="true"
           label-mode="true"
           label-mode="true"
+          debounce="true"
           css-class="query-segment-value"
           css-class="query-segment-value"
           placeholder="Tag value"
           placeholder="Tag value"
           get-options="ctrl.getTagValues(tag, $index, $query)"
           get-options="ctrl.getTagValues(tag, $index, $query)"
@@ -45,7 +43,7 @@
       </div>
       </div>
 
 
       <div ng-if="ctrl.queryModel.seriesByTagUsed" ng-repeat="segment in ctrl.addTagSegments" role="menuitem" class="gf-form">
       <div ng-if="ctrl.queryModel.seriesByTagUsed" ng-repeat="segment in ctrl.addTagSegments" role="menuitem" class="gf-form">
-        <metric-segment segment="segment" get-options="ctrl.getTagsAsSegments($query)" on-change="ctrl.addNewTag(segment)" />
+        <metric-segment segment="segment" get-options="ctrl.getTagsAsSegments($query)" on-change="ctrl.addNewTag(segment)" debounce="true" />
       </div>
       </div>
 
 
       <div ng-if="!ctrl.queryModel.seriesByTagUsed" ng-repeat="segment in ctrl.segments" role="menuitem" class="gf-form">
       <div ng-if="!ctrl.queryModel.seriesByTagUsed" ng-repeat="segment in ctrl.segments" role="menuitem" class="gf-form">

+ 1 - 1
public/app/stores/FolderStore/FolderStore.ts

@@ -53,6 +53,6 @@ export const FolderStore = types
     deleteFolder: flow(function* deleteFolder() {
     deleteFolder: flow(function* deleteFolder() {
       const backendSrv = getEnv(self).backendSrv;
       const backendSrv = getEnv(self).backendSrv;
 
 
-      return backendSrv.deleteDashboard(self.folder.url);
+      return backendSrv.deleteDashboard(self.folder.uid);
     }),
     }),
   }));
   }));

+ 2 - 0
public/app/stores/PermissionsStore/PermissionsStore.ts

@@ -108,6 +108,8 @@ export const PermissionsStore = types
         self.isFolder = isFolder;
         self.isFolder = isFolder;
         self.isInRoot = isInRoot;
         self.isInRoot = isInRoot;
         self.dashboardId = dashboardId;
         self.dashboardId = dashboardId;
+        self.items.clear();
+
         const res = yield backendSrv.get(`/api/dashboards/id/${dashboardId}/acl`);
         const res = yield backendSrv.get(`/api/dashboards/id/${dashboardId}/acl`);
         const items = prepareServerResponse(res, dashboardId, isFolder, isInRoot);
         const items = prepareServerResponse(res, dashboardId, isFolder, isInRoot);
         self.items = items;
         self.items = items;

+ 9 - 2
public/sass/components/_dashboard_grid.scss

@@ -41,8 +41,15 @@
 
 
 .theme-dark {
 .theme-dark {
   .react-grid-item > .react-resizable-handle::after {
   .react-grid-item > .react-resizable-handle::after {
-    border-right: 2px solid rgba(255, 255, 255, 0.4);
-    border-bottom: 2px solid rgba(255, 255, 255, 0.4);
+    border-right: 2px solid $gray-4;
+    border-bottom: 2px solid $gray-4;
+  }
+}
+
+.theme-light {
+  .react-grid-item > .react-resizable-handle::after {
+    border-right: 2px solid $gray-3;
+    border-bottom: 2px solid $gray-3;
   }
   }
 }
 }