Selaa lähdekoodia

WIP: add user group search

Daniel Lee 8 vuotta sitten
vanhempi
commit
233cd7af4a

+ 7 - 0
pkg/api/api.go

@@ -132,6 +132,13 @@ func (hs *HttpServer) registerRoutes() {
 			r.Post("/:id/using/:orgId", wrap(UpdateUserActiveOrg))
 		}, reqGrafanaAdmin)
 
+		// user group (admin permission required)
+		r.Group("/user-groups", func() {
+			r.Get("/search", wrap(SearchUserGroups))
+			r.Post("/", quota("user-groups"), bind(m.CreateUserGroupCommand{}), wrap(CreateUserGroup))
+			r.Delete("/:userGroupId", wrap(DeleteUserGroupById))
+		}, reqGrafanaAdmin)
+
 		// org information available to all users.
 		r.Group("/org", func() {
 			r.Get("/", wrap(GetOrgCurrent))

+ 1 - 1
pkg/api/user.go

@@ -218,7 +218,7 @@ func SearchUsers(c *middleware.Context) Response {
 	return Json(200, query.Result.Users)
 }
 
-// GET /api/search
+// GET /api/users/search
 func SearchUsersWithPaging(c *middleware.Context) Response {
 	query, err := searchUser(c)
 	if err != nil {

+ 66 - 0
pkg/api/user_group.go

@@ -0,0 +1,66 @@
+package api
+
+import (
+	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/metrics"
+	"github.com/grafana/grafana/pkg/middleware"
+	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/util"
+)
+
+// POST /api/user-groups
+func CreateUserGroup(c *middleware.Context, cmd m.CreateUserGroupCommand) Response {
+	cmd.OrgId = c.OrgId
+	if err := bus.Dispatch(&cmd); err != nil {
+		if err == m.ErrUserGroupNameTaken {
+			return ApiError(409, "User Group name taken", err)
+		}
+		return ApiError(500, "Failed to create User Group", err)
+	}
+
+	metrics.M_Api_UserGroup_Create.Inc(1)
+
+	return Json(200, &util.DynMap{
+		"userGroupId": cmd.Result.Id,
+		"message":     "User Group created",
+	})
+}
+
+// DELETE /api/user-groups/:userGroupId
+func DeleteUserGroupById(c *middleware.Context) Response {
+	if err := bus.Dispatch(&m.DeleteUserGroupCommand{Id: c.ParamsInt64(":userGroupId")}); err != nil {
+		if err == m.ErrUserGroupNotFound {
+			return ApiError(404, "Failed to delete User Group. ID not found", nil)
+		}
+		return ApiError(500, "Failed to update User Group", err)
+	}
+	return ApiSuccess("User Group deleted")
+}
+
+// GET /api/user-groups/search
+func SearchUserGroups(c *middleware.Context) Response {
+	perPage := c.QueryInt("perpage")
+	if perPage <= 0 {
+		perPage = 1000
+	}
+	page := c.QueryInt("page")
+	if page < 1 {
+		page = 1
+	}
+
+	query := m.SearchUserGroupsQuery{
+		Query: c.Query("query"),
+		Name:  c.Query("name"),
+		Page:  page,
+		Limit: perPage,
+	}
+
+	if err := bus.Dispatch(&query); err != nil {
+		return ApiError(500, "Failed to search User Groups", err)
+	}
+
+	query.Result.Page = page
+	query.Result.PerPage = perPage
+
+	return Json(200, query.Result)
+}

+ 71 - 0
pkg/api/user_group_test.go

@@ -0,0 +1,71 @@
+package api
+
+import (
+	"testing"
+
+	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/components/simplejson"
+	"github.com/grafana/grafana/pkg/models"
+
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestUserGroupApiEndpoint(t *testing.T) {
+	Convey("Given two user groups", t, func() {
+		mockResult := models.SearchUserGroupQueryResult{
+			UserGroups: []*models.UserGroup{
+				{Name: "userGroup1"},
+				{Name: "userGroup2"},
+			},
+			TotalCount: 2,
+		}
+
+		Convey("When searching with no parameters", func() {
+			loggedInUserScenario("When calling GET on", "/api/user-groups/search", func(sc *scenarioContext) {
+				var sentLimit int
+				var sendPage int
+				bus.AddHandler("test", func(query *models.SearchUserGroupsQuery) error {
+					query.Result = mockResult
+
+					sentLimit = query.Limit
+					sendPage = query.Page
+
+					return nil
+				})
+
+				sc.handlerFunc = SearchUserGroups
+				sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
+
+				So(sentLimit, ShouldEqual, 1000)
+				So(sendPage, ShouldEqual, 1)
+
+				respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
+				So(err, ShouldBeNil)
+
+				So(respJSON.Get("totalCount").MustInt(), ShouldEqual, 2)
+				So(len(respJSON.Get("userGroups").MustArray()), ShouldEqual, 2)
+			})
+		})
+
+		Convey("When searching with page and perpage parameters", func() {
+			loggedInUserScenario("When calling GET on", "/api/user-groups/search", func(sc *scenarioContext) {
+				var sentLimit int
+				var sendPage int
+				bus.AddHandler("test", func(query *models.SearchUserGroupsQuery) error {
+					query.Result = mockResult
+
+					sentLimit = query.Limit
+					sendPage = query.Page
+
+					return nil
+				})
+
+				sc.handlerFunc = SearchUserGroups
+				sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec()
+
+				So(sentLimit, ShouldEqual, 10)
+				So(sendPage, ShouldEqual, 2)
+			})
+		})
+	})
+}

+ 3 - 0
pkg/metrics/metrics.go

@@ -35,6 +35,7 @@ var (
 	M_Api_Dashboard_Snapshot_Create        Counter
 	M_Api_Dashboard_Snapshot_External      Counter
 	M_Api_Dashboard_Snapshot_Get           Counter
+	M_Api_UserGroup_Create                 Counter
 	M_Models_Dashboard_Insert              Counter
 	M_Alerting_Result_State_Alerting       Counter
 	M_Alerting_Result_State_Ok             Counter
@@ -92,6 +93,8 @@ func initMetricVars(settings *MetricSettings) {
 	M_Api_User_SignUpCompleted = RegCounter("api.user.signup_completed")
 	M_Api_User_SignUpInvite = RegCounter("api.user.signup_invite")
 
+	M_Api_UserGroup_Create = RegCounter("api.usergroup.create")
+
 	M_Api_Dashboard_Save = RegTimer("api.dashboard.save")
 	M_Api_Dashboard_Get = RegTimer("api.dashboard.get")
 	M_Api_Dashboard_Search = RegTimer("api.dashboard.search")

+ 14 - 7
pkg/models/user_group.go

@@ -13,12 +13,12 @@ var (
 
 // UserGroup model
 type UserGroup struct {
-	Id    int64
-	OrgId int64
-	Name  string
+	Id    int64  `json:"id"`
+	OrgId int64  `json:"orgId"`
+	Name  string `json:"name"`
 
-	Created time.Time
-	Updated time.Time
+	Created time.Time `json:"created"`
+	Updated time.Time `json:"updated"`
 }
 
 // ---------------------
@@ -26,7 +26,7 @@ type UserGroup struct {
 
 type CreateUserGroupCommand struct {
 	Name  string `json:"name" binding:"Required"`
-	OrgId int64  `json:"orgId" binding:"Required"`
+	OrgId int64  `json:"-"`
 
 	Result UserGroup `json:"-"`
 }
@@ -46,5 +46,12 @@ type SearchUserGroupsQuery struct {
 	Limit int
 	Page  int
 
-	Result []*UserGroup
+	Result SearchUserGroupQueryResult
+}
+
+type SearchUserGroupQueryResult struct {
+	TotalCount int64        `json:"totalCount"`
+	UserGroups []*UserGroup `json:"userGroups"`
+	Page       int          `json:"page"`
+	PerPage    int          `json:"perPage"`
 }

+ 24 - 4
pkg/services/sqlstore/user_group.go

@@ -83,17 +83,37 @@ func isUserGroupNameTaken(name string, existingId int64, sess *session) (bool, e
 }
 
 func SearchUserGroups(query *m.SearchUserGroupsQuery) error {
-	query.Result = make([]*m.UserGroup, 0)
+	query.Result = m.SearchUserGroupQueryResult{
+		UserGroups: make([]*m.UserGroup, 0),
+	}
+	queryWithWildcards := "%" + query.Query + "%"
+
 	sess := x.Table("user_group")
 	if query.Query != "" {
-		sess.Where("name LIKE ?", query.Query+"%")
+		sess.Where("name LIKE ?", queryWithWildcards)
 	}
 	if query.Name != "" {
 		sess.Where("name=?", query.Name)
 	}
-	sess.Limit(query.Limit, query.Limit*query.Page)
+	offset := query.Limit * (query.Page - 1)
+	sess.Limit(query.Limit, offset)
 	sess.Cols("id", "name")
-	err := sess.Find(&query.Result)
+	if err := sess.Find(&query.Result.UserGroups); err != nil {
+		return err
+	}
+
+	userGroup := m.UserGroup{}
+
+	countSess := x.Table("user_group")
+	if query.Query != "" {
+		countSess.Where("name LIKE ?", queryWithWildcards)
+	}
+	if query.Name != "" {
+		countSess.Where("name=?", query.Name)
+	}
+	count, err := countSess.Count(&userGroup)
+	query.Result.TotalCount = count
+
 	return err
 }
 

+ 12 - 6
pkg/services/sqlstore/user_group_test.go

@@ -36,13 +36,13 @@ func TestUserGroupCommandsAndQueries(t *testing.T) {
 			So(err, ShouldBeNil)
 
 			Convey("Should be able to create user groups and add users", func() {
-				query := &m.SearchUserGroupsQuery{Name: "group1 name"}
+				query := &m.SearchUserGroupsQuery{Name: "group1 name", Page: 1, Limit: 10}
 				err = SearchUserGroups(query)
 				So(err, ShouldBeNil)
-				So(query.Page, ShouldEqual, 0)
+				So(query.Page, ShouldEqual, 1)
 
-				userGroup1 := query.Result[0]
-				So(query.Result[0].Name, ShouldEqual, "group1 name")
+				userGroup1 := query.Result.UserGroups[0]
+				So(userGroup1.Name, ShouldEqual, "group1 name")
 
 				err = AddUserGroupMember(&m.AddUserGroupMemberCommand{OrgId: 1, UserGroupId: userGroup1.Id, UserId: userIds[0]})
 				So(err, ShouldBeNil)
@@ -55,10 +55,16 @@ func TestUserGroupCommandsAndQueries(t *testing.T) {
 			})
 
 			Convey("Should be able to search for user groups", func() {
-				query := &m.SearchUserGroupsQuery{Query: "group"}
+				query := &m.SearchUserGroupsQuery{Query: "group", Page: 1}
 				err = SearchUserGroups(query)
 				So(err, ShouldBeNil)
-				So(len(query.Result), ShouldEqual, 2)
+				So(len(query.Result.UserGroups), ShouldEqual, 2)
+				So(query.Result.TotalCount, ShouldEqual, 2)
+
+				query2 := &m.SearchUserGroupsQuery{Query: ""}
+				err = SearchUserGroups(query2)
+				So(err, ShouldBeNil)
+				So(len(query2.Result.UserGroups), ShouldEqual, 2)
 			})
 
 			Convey("Should be able to remove users from a group", func() {

+ 4 - 0
public/app/core/components/sidemenu/sidemenu.ts

@@ -64,6 +64,10 @@ export class SideMenuCtrl {
        text: "Users",
        url: this.getUrl("/org/users")
      });
+     this.orgMenu.push({
+       text: "User Groups",
+       url: this.getUrl("/org/user-groups")
+     });
      this.orgMenu.push({
        text: "API Keys",
        url: this.getUrl("/org/apikeys")

+ 6 - 0
public/app/core/routes/routes.ts

@@ -83,6 +83,12 @@ function setupAngularRoutes($routeProvider, $locationProvider) {
     controller : 'OrgApiKeysCtrl',
     resolve: loadOrgBundle,
   })
+  .when('/org/user-groups', {
+    templateUrl: 'public/app/features/org/partials/user_groups.html',
+    controller : 'UserGroupsCtrl',
+    controllerAs: 'ctrl',
+    resolve: loadOrgBundle,
+  })
   .when('/profile', {
     templateUrl: 'public/app/features/org/partials/profile.html',
     controller : 'ProfileCtrl',

+ 1 - 1
public/app/features/org/all.js

@@ -1,7 +1,6 @@
 define([
   './org_users_ctrl',
   './profile_ctrl',
-  './org_users_ctrl',
   './select_org_ctrl',
   './change_password_ctrl',
   './newOrgCtrl',
@@ -9,4 +8,5 @@ define([
   './orgApiKeysCtrl',
   './orgDetailsCtrl',
   './prefs_control',
+  './user_groups_ctrl',
 ], function () {});

+ 60 - 0
public/app/features/org/partials/user_groups.html

@@ -0,0 +1,60 @@
+<navbar icon="icon-gf icon-gf-users" title="User Groups" title-url="org">
+</navbar>
+
+<div class="page-container">
+	<div class="page-header">
+		<h1>User Groups</h1>
+
+    <div class="page-header-tabs">
+      <a class="btn btn-success" href="/org/user-groups/create">
+        <i class="fa fa-plus"></i>
+          Create User Group
+      </a>
+    </div>
+  </div>
+  <div class="search-field-wrapper pull-right width-18">
+    <span style="position: relative;">
+      <input  type="text" placeholder="Find User Group by name" tabindex="1" give-focus="true"
+      ng-model="ctrl.query" ng-model-options="{ debounce: 500 }" spellcheck='false' ng-change="ctrl.get()" />
+    </span>
+  </div>
+  <div class="admin-list-table">
+    <table class="filter-table form-inline">
+      <thead>
+        <tr>
+          <th>Id</th>
+          <th>Name</th>
+          <th></th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr ng-repeat="userGroup in ctrl.userGroups">
+          <td>{{userGroup.id}}</td>
+          <td>{{userGroup.name}}</td>
+          <td class="text-right">
+            <a href="org/user-groups/edit/{{userGroup.id}}" class="btn btn-inverse btn-small">
+              <i class="fa fa-edit"></i>
+              Edit
+            </a>
+            &nbsp;&nbsp;
+            <a ng-click="ctrl.deleteUserGroup(userGroup)" class="btn btn-danger btn-small">
+              <i class="fa fa-remove"></i>
+            </a>
+          </td>
+        </tr>
+      </tbody>
+
+    </table>
+  </div>
+
+  <div class="admin-list-paging" ng-if="ctrl.showPaging">
+    <ol>
+      <li ng-repeat="page in ctrl.pages">
+        <button
+          class="btn btn-small"
+          ng-class="{'btn-secondary': page.current, 'btn-inverse': !page.current}"
+          ng-click="ctrl.navigateToPage(page)">{{page.page}}</button>
+      </li>
+    </ol>
+  </div>
+</div>

+ 68 - 0
public/app/features/org/user_groups_ctrl.ts

@@ -0,0 +1,68 @@
+///<reference path="../../headers/common.d.ts" />
+
+import coreModule from 'app/core/core_module';
+
+export default class UserGroupsCtrl {
+  userGroups: any;
+  pages = [];
+  perPage = 50;
+  page = 1;
+  totalPages: number;
+  showPaging = false;
+  query: any = '';
+
+  /** @ngInject */
+  constructor(private $scope, private $http, private backendSrv) {
+    this.get();
+  }
+
+  get() {
+    this.backendSrv.get(`/api/user-groups/search?perpage=${this.perPage}&page=${this.page}&query=${this.query}`)
+      .then((result) => {
+        this.userGroups = result.userGroups;
+        this.page = result.page;
+        this.perPage = result.perPage;
+        this.totalPages = Math.ceil(result.totalCount / result.perPage);
+        this.showPaging = this.totalPages > 1;
+        this.pages = [];
+
+        for (var i = 1; i < this.totalPages+1; i++) {
+          this.pages.push({ page: i, current: i === this.page});
+        }
+      });
+  }
+
+  navigateToPage(page) {
+    this.page = page.page;
+    this.get();
+  }
+
+  deleteUserGroup(userGroup) {
+    this.$scope.appEvent('confirm-modal', {
+      title: 'Delete',
+      text: 'Are you sure you want to delete User Group ' + userGroup.name + '?',
+      yesText: "Delete",
+      icon: "fa-warning",
+      onConfirm: () => {
+        this.deleteUserGroupConfirmed(userGroup);
+      }
+    });
+  }
+
+  deleteUserGroupConfirmed(userGroup) {
+    this.backendSrv.delete('/api/user-groups/' + userGroup.id)
+      .then(this.get.bind(this));
+  }
+
+  openUserGroupModal() {
+    var modalScope = this.$scope.$new();
+
+    this.$scope.appEvent('show-modal', {
+      src: 'public/app/features/org/partials/add_user.html',
+      modalClass: 'user-group-modal',
+      scope: modalScope
+    });
+  }
+}
+
+coreModule.controller('UserGroupsCtrl', UserGroupsCtrl);