Commit 233cd7af by Daniel Lee

WIP: add user group search

parent af67aea2
...@@ -132,6 +132,13 @@ func (hs *HttpServer) registerRoutes() { ...@@ -132,6 +132,13 @@ func (hs *HttpServer) registerRoutes() {
r.Post("/:id/using/:orgId", wrap(UpdateUserActiveOrg)) r.Post("/:id/using/:orgId", wrap(UpdateUserActiveOrg))
}, reqGrafanaAdmin) }, 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. // org information available to all users.
r.Group("/org", func() { r.Group("/org", func() {
r.Get("/", wrap(GetOrgCurrent)) r.Get("/", wrap(GetOrgCurrent))
......
...@@ -218,7 +218,7 @@ func SearchUsers(c *middleware.Context) Response { ...@@ -218,7 +218,7 @@ func SearchUsers(c *middleware.Context) Response {
return Json(200, query.Result.Users) return Json(200, query.Result.Users)
} }
// GET /api/search // GET /api/users/search
func SearchUsersWithPaging(c *middleware.Context) Response { func SearchUsersWithPaging(c *middleware.Context) Response {
query, err := searchUser(c) query, err := searchUser(c)
if err != nil { if err != nil {
......
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)
}
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)
})
})
})
}
...@@ -35,6 +35,7 @@ var ( ...@@ -35,6 +35,7 @@ var (
M_Api_Dashboard_Snapshot_Create Counter M_Api_Dashboard_Snapshot_Create Counter
M_Api_Dashboard_Snapshot_External Counter M_Api_Dashboard_Snapshot_External Counter
M_Api_Dashboard_Snapshot_Get Counter M_Api_Dashboard_Snapshot_Get Counter
M_Api_UserGroup_Create Counter
M_Models_Dashboard_Insert Counter M_Models_Dashboard_Insert Counter
M_Alerting_Result_State_Alerting Counter M_Alerting_Result_State_Alerting Counter
M_Alerting_Result_State_Ok Counter M_Alerting_Result_State_Ok Counter
...@@ -92,6 +93,8 @@ func initMetricVars(settings *MetricSettings) { ...@@ -92,6 +93,8 @@ func initMetricVars(settings *MetricSettings) {
M_Api_User_SignUpCompleted = RegCounter("api.user.signup_completed") M_Api_User_SignUpCompleted = RegCounter("api.user.signup_completed")
M_Api_User_SignUpInvite = RegCounter("api.user.signup_invite") 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_Save = RegTimer("api.dashboard.save")
M_Api_Dashboard_Get = RegTimer("api.dashboard.get") M_Api_Dashboard_Get = RegTimer("api.dashboard.get")
M_Api_Dashboard_Search = RegTimer("api.dashboard.search") M_Api_Dashboard_Search = RegTimer("api.dashboard.search")
......
...@@ -13,12 +13,12 @@ var ( ...@@ -13,12 +13,12 @@ var (
// UserGroup model // UserGroup model
type UserGroup struct { type UserGroup struct {
Id int64 Id int64 `json:"id"`
OrgId int64 OrgId int64 `json:"orgId"`
Name string Name string `json:"name"`
Created time.Time Created time.Time `json:"created"`
Updated time.Time Updated time.Time `json:"updated"`
} }
// --------------------- // ---------------------
...@@ -26,7 +26,7 @@ type UserGroup struct { ...@@ -26,7 +26,7 @@ type UserGroup struct {
type CreateUserGroupCommand struct { type CreateUserGroupCommand struct {
Name string `json:"name" binding:"Required"` Name string `json:"name" binding:"Required"`
OrgId int64 `json:"orgId" binding:"Required"` OrgId int64 `json:"-"`
Result UserGroup `json:"-"` Result UserGroup `json:"-"`
} }
...@@ -46,5 +46,12 @@ type SearchUserGroupsQuery struct { ...@@ -46,5 +46,12 @@ type SearchUserGroupsQuery struct {
Limit int Limit int
Page 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"`
} }
...@@ -83,17 +83,37 @@ func isUserGroupNameTaken(name string, existingId int64, sess *session) (bool, e ...@@ -83,17 +83,37 @@ func isUserGroupNameTaken(name string, existingId int64, sess *session) (bool, e
} }
func SearchUserGroups(query *m.SearchUserGroupsQuery) error { 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") sess := x.Table("user_group")
if query.Query != "" { if query.Query != "" {
sess.Where("name LIKE ?", query.Query+"%") sess.Where("name LIKE ?", queryWithWildcards)
} }
if query.Name != "" { if query.Name != "" {
sess.Where("name=?", 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") 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 return err
} }
......
...@@ -36,13 +36,13 @@ func TestUserGroupCommandsAndQueries(t *testing.T) { ...@@ -36,13 +36,13 @@ func TestUserGroupCommandsAndQueries(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
Convey("Should be able to create user groups and add users", func() { 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) err = SearchUserGroups(query)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(query.Page, ShouldEqual, 0) So(query.Page, ShouldEqual, 1)
userGroup1 := query.Result[0] userGroup1 := query.Result.UserGroups[0]
So(query.Result[0].Name, ShouldEqual, "group1 name") So(userGroup1.Name, ShouldEqual, "group1 name")
err = AddUserGroupMember(&m.AddUserGroupMemberCommand{OrgId: 1, UserGroupId: userGroup1.Id, UserId: userIds[0]}) err = AddUserGroupMember(&m.AddUserGroupMemberCommand{OrgId: 1, UserGroupId: userGroup1.Id, UserId: userIds[0]})
So(err, ShouldBeNil) So(err, ShouldBeNil)
...@@ -55,10 +55,16 @@ func TestUserGroupCommandsAndQueries(t *testing.T) { ...@@ -55,10 +55,16 @@ func TestUserGroupCommandsAndQueries(t *testing.T) {
}) })
Convey("Should be able to search for user groups", func() { 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) err = SearchUserGroups(query)
So(err, ShouldBeNil) 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() { Convey("Should be able to remove users from a group", func() {
......
...@@ -65,6 +65,10 @@ export class SideMenuCtrl { ...@@ -65,6 +65,10 @@ export class SideMenuCtrl {
url: this.getUrl("/org/users") url: this.getUrl("/org/users")
}); });
this.orgMenu.push({ this.orgMenu.push({
text: "User Groups",
url: this.getUrl("/org/user-groups")
});
this.orgMenu.push({
text: "API Keys", text: "API Keys",
url: this.getUrl("/org/apikeys") url: this.getUrl("/org/apikeys")
}); });
......
...@@ -83,6 +83,12 @@ function setupAngularRoutes($routeProvider, $locationProvider) { ...@@ -83,6 +83,12 @@ function setupAngularRoutes($routeProvider, $locationProvider) {
controller : 'OrgApiKeysCtrl', controller : 'OrgApiKeysCtrl',
resolve: loadOrgBundle, resolve: loadOrgBundle,
}) })
.when('/org/user-groups', {
templateUrl: 'public/app/features/org/partials/user_groups.html',
controller : 'UserGroupsCtrl',
controllerAs: 'ctrl',
resolve: loadOrgBundle,
})
.when('/profile', { .when('/profile', {
templateUrl: 'public/app/features/org/partials/profile.html', templateUrl: 'public/app/features/org/partials/profile.html',
controller : 'ProfileCtrl', controller : 'ProfileCtrl',
......
define([ define([
'./org_users_ctrl', './org_users_ctrl',
'./profile_ctrl', './profile_ctrl',
'./org_users_ctrl',
'./select_org_ctrl', './select_org_ctrl',
'./change_password_ctrl', './change_password_ctrl',
'./newOrgCtrl', './newOrgCtrl',
...@@ -9,4 +8,5 @@ define([ ...@@ -9,4 +8,5 @@ define([
'./orgApiKeysCtrl', './orgApiKeysCtrl',
'./orgDetailsCtrl', './orgDetailsCtrl',
'./prefs_control', './prefs_control',
'./user_groups_ctrl',
], function () {}); ], function () {});
<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>
///<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);
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment