From 3242354a4b463dd16b05386bdec37615441bca87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 20 Jul 2015 10:57:39 +0200 Subject: [PATCH] feat(invite): worked on pending invitations list, revoke invite now works, #2353 --- pkg/api/api.go | 1 + pkg/api/org_invite.go | 19 +++++++- pkg/models/temp_user.go | 25 ++++++++-- pkg/services/sqlstore/migrations/temp_user.go | 12 +++-- pkg/services/sqlstore/temp_user.go | 19 ++++++-- pkg/services/sqlstore/temp_user_test.go | 16 +++++-- public/app/features/org/orgUsersCtrl.js | 5 +- public/app/features/org/partials/invite.html | 7 ++- .../app/features/org/partials/orgUsers.html | 46 +++++++++++-------- public/app/features/org/userInviteCtrl.js | 5 +- public/css/less/tables_lists.less | 26 +++-------- 11 files changed, 116 insertions(+), 65 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index a0e136ffd8c..e3cfe9e6788 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -94,6 +94,7 @@ func Register(r *macaron.Macaron) { // invites r.Get("/invites", wrap(GetPendingOrgInvites)) r.Post("/invites", bind(dtos.AddInviteForm{}), wrap(AddOrgInvite)) + r.Patch("/invites/:id/revoke", wrap(RevokeInvite)) }, regOrgAdmin) // create new org diff --git a/pkg/api/org_invite.go b/pkg/api/org_invite.go index 20f2221b4d5..0e9cc9a8da2 100644 --- a/pkg/api/org_invite.go +++ b/pkg/api/org_invite.go @@ -10,7 +10,7 @@ import ( ) func GetPendingOrgInvites(c *middleware.Context) Response { - query := m.GetTempUsersForOrgQuery{OrgId: c.OrgId} + query := m.GetTempUsersForOrgQuery{OrgId: c.OrgId, Status: m.TmpUserInvitePending} if err := bus.Dispatch(&query); err != nil { return ApiError(500, "Failed to get invites from db", err) @@ -47,10 +47,11 @@ func AddOrgInvite(c *middleware.Context, inviteDto dtos.AddInviteForm) Response cmd.OrgId = c.OrgId cmd.Email = inviteDto.Email cmd.Name = inviteDto.Name - cmd.IsInvite = true + cmd.Status = m.TmpUserInvitePending cmd.InvitedByUserId = c.UserId cmd.Code = util.GetRandomString(30) cmd.Role = inviteDto.Role + cmd.RemoteAddr = c.Req.RemoteAddr if err := bus.Dispatch(&cmd); err != nil { return ApiError(500, "Failed to save invite to database", err) @@ -77,3 +78,17 @@ func AddOrgInvite(c *middleware.Context, inviteDto dtos.AddInviteForm) Response return ApiSuccess("ok, done!") } + +func RevokeInvite(c *middleware.Context) Response { + cmd := m.UpdateTempUserStatusCommand{ + Id: c.ParamsInt64(":id"), + OrgId: c.OrgId, + Status: m.TmpUserRevoked, + } + + if err := bus.Dispatch(&cmd); err != nil { + return ApiError(500, "Failed to update invite status", err) + } + + return ApiSuccess("Invite revoked") +} diff --git a/pkg/models/temp_user.go b/pkg/models/temp_user.go index 8e06ade3cb2..13915c04bb8 100644 --- a/pkg/models/temp_user.go +++ b/pkg/models/temp_user.go @@ -10,6 +10,15 @@ var ( ErrTempUserNotFound = errors.New("User not found") ) +type TempUserStatus string + +const ( + TmpUserInvitePending TempUserStatus = "InvitePending" + TmpUserCompleted TempUserStatus = "Completed" + TmpUserEmailPending TempUserStatus = "EmailPending" + TmpUserRevoked TempUserStatus = "Revoked" +) + // TempUser holds data for org invites and unconfirmed sign ups type TempUser struct { Id int64 @@ -18,12 +27,13 @@ type TempUser struct { Email string Name string Role RoleType - IsInvite bool InvitedByUserId int64 + Status TempUserStatus EmailSent bool EmailSentOn time.Time Code string + RemoteAddr string Created time.Time Updated time.Time @@ -36,16 +46,24 @@ type CreateTempUserCommand struct { Email string Name string OrgId int64 - IsInvite bool InvitedByUserId int64 + Status TempUserStatus Code string Role RoleType + RemoteAddr string Result *TempUser } +type UpdateTempUserStatusCommand struct { + Id int64 + OrgId int64 + Status TempUserStatus +} + type GetTempUsersForOrgQuery struct { - OrgId int64 + OrgId int64 + Status TempUserStatus Result []*TempUserDTO } @@ -56,6 +74,7 @@ type TempUserDTO struct { Email string `json:"email"` Role string `json:"role"` InvitedBy string `json:"invitedBy"` + Code string `json:"code"` EmailSent bool `json:"emailSent"` EmailSentOn time.Time `json:"emailSentOn"` Created time.Time `json:"createdOn"` diff --git a/pkg/services/sqlstore/migrations/temp_user.go b/pkg/services/sqlstore/migrations/temp_user.go index 6453e659676..36ae2706e57 100644 --- a/pkg/services/sqlstore/migrations/temp_user.go +++ b/pkg/services/sqlstore/migrations/temp_user.go @@ -13,10 +13,11 @@ func addTempUserMigrations(mg *Migrator) { {Name: "name", Type: DB_NVarchar, Length: 255, Nullable: true}, {Name: "role", Type: DB_NVarchar, Length: 20, Nullable: true}, {Name: "code", Type: DB_NVarchar, Length: 255}, - {Name: "is_invite", Type: DB_Bool}, + {Name: "status", Type: DB_Varchar, Length: 20}, {Name: "invited_by_user_id", Type: DB_BigInt, Nullable: true}, {Name: "email_sent", Type: DB_Bool}, {Name: "email_sent_on", Type: DB_DateTime, Nullable: true}, + {Name: "remote_addr", Type: DB_Varchar, Nullable: true}, {Name: "created", Type: DB_DateTime}, {Name: "updated", Type: DB_DateTime}, }, @@ -24,11 +25,14 @@ func addTempUserMigrations(mg *Migrator) { {Cols: []string{"email"}, Type: IndexType}, {Cols: []string{"org_id"}, Type: IndexType}, {Cols: []string{"code"}, Type: IndexType}, + {Cols: []string{"status"}, Type: IndexType}, }, } - // create table - mg.AddMigration("create temp user table v1-3", NewAddTableMigration(tempUserV1)) + // addDropAllIndicesMigrations(mg, "v7", tempUserV1) + // mg.AddMigration("Drop old table tempUser v7", NewDropTableMigration("temp_user")) - addTableIndicesMigrations(mg, "v1-3", tempUserV1) + // create table + mg.AddMigration("create temp user table v1-7", NewAddTableMigration(tempUserV1)) + addTableIndicesMigrations(mg, "v1-7", tempUserV1) } diff --git a/pkg/services/sqlstore/temp_user.go b/pkg/services/sqlstore/temp_user.go index c80e015fa0f..9e8ce5be4b2 100644 --- a/pkg/services/sqlstore/temp_user.go +++ b/pkg/services/sqlstore/temp_user.go @@ -3,6 +3,7 @@ package sqlstore import ( "time" + "github.com/go-xorm/xorm" "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" ) @@ -10,6 +11,15 @@ import ( func init() { bus.AddHandler("sql", CreateTempUser) bus.AddHandler("sql", GetTempUsersForOrg) + bus.AddHandler("sql", UpdateTempUserStatus) +} + +func UpdateTempUserStatus(cmd *m.UpdateTempUserStatusCommand) error { + return inTransaction(func(sess *xorm.Session) error { + var rawSql = "UPDATE temp_user SET status=? WHERE id=? and org_id=?" + _, err := sess.Exec(rawSql, string(cmd.Status), cmd.Id, cmd.OrgId) + return err + }) } func CreateTempUser(cmd *m.CreateTempUserCommand) error { @@ -22,14 +32,13 @@ func CreateTempUser(cmd *m.CreateTempUserCommand) error { OrgId: cmd.OrgId, Code: cmd.Code, Role: cmd.Role, - IsInvite: cmd.IsInvite, + Status: cmd.Status, + RemoteAddr: cmd.RemoteAddr, InvitedByUserId: cmd.InvitedByUserId, Created: time.Now(), Updated: time.Now(), } - sess.UseBool("is_invite") - if _, err := sess.Insert(user); err != nil { return err } @@ -51,10 +60,10 @@ func GetTempUsersForOrg(query *m.GetTempUsersForOrgQuery) error { u.login as invited_by FROM ` + dialect.Quote("temp_user") + ` as tu LEFT OUTER JOIN ` + dialect.Quote("user") + ` as u on u.id = tu.invited_by_user_id - WHERE tu.org_id=? ORDER BY tu.created desc` + WHERE tu.org_id=? AND tu.status =? ORDER BY tu.created desc` query.Result = make([]*m.TempUserDTO, 0) - sess := x.Sql(rawSql, query.OrgId) + sess := x.Sql(rawSql, query.OrgId, string(query.Status)) err := sess.Find(&query.Result) return err } diff --git a/pkg/services/sqlstore/temp_user_test.go b/pkg/services/sqlstore/temp_user_test.go index 9424ae09a15..c7c599b3db4 100644 --- a/pkg/services/sqlstore/temp_user_test.go +++ b/pkg/services/sqlstore/temp_user_test.go @@ -15,22 +15,28 @@ func TestTempUserCommandsAndQueries(t *testing.T) { Convey("Given saved api key", func() { cmd := m.CreateTempUserCommand{ - OrgId: 2256, - Name: "hello", - Email: "e@as.co", - IsInvite: true, + OrgId: 2256, + Name: "hello", + Email: "e@as.co", + Status: m.TmpUserInvitePending, } err := CreateTempUser(&cmd) So(err, ShouldBeNil) Convey("Should be able to get temp users by org id", func() { - query := m.GetTempUsersForOrgQuery{OrgId: 2256} + query := m.GetTempUsersForOrgQuery{OrgId: 2256, Status: m.TmpUserInvitePending} err = GetTempUsersForOrg(&query) So(err, ShouldBeNil) So(len(query.Result), ShouldEqual, 1) }) + Convey("Should be able update status", func() { + cmd2 := m.UpdateTempUserStatusCommand{OrgId: 2256, Status: m.TmpUserRevoked, Id: cmd.Result.Id} + err := UpdateTempUserStatus(&cmd2) + So(err, ShouldBeNil) + }) + }) }) } diff --git a/public/app/features/org/orgUsersCtrl.js b/public/app/features/org/orgUsersCtrl.js index ea3d97d7337..e8543c98017 100644 --- a/public/app/features/org/orgUsersCtrl.js +++ b/public/app/features/org/orgUsersCtrl.js @@ -38,9 +38,8 @@ function (angular) { backendSrv.delete('/api/org/users/' + user.userId).then($scope.get); }; - $scope.addUser = function() { - if (!$scope.form.$valid) { return; } - backendSrv.post('/api/org/users', $scope.user).then($scope.get); + $scope.revokeInvite = function(invite) { + backendSrv.patch('/api/org/invites/' + invite.id + '/revoke').then($scope.get); }; $scope.openInviteModal = function() { diff --git a/public/app/features/org/partials/invite.html b/public/app/features/org/partials/invite.html index 53290bd6c30..6a18c816c68 100644 --- a/public/app/features/org/partials/invite.html +++ b/public/app/features/org/partials/invite.html @@ -52,14 +52,17 @@ -
-
+
+ Invite another +
+ +
+
diff --git a/public/app/features/org/partials/orgUsers.html b/public/app/features/org/partials/orgUsers.html index 8a88cdc969c..be011b31232 100644 --- a/public/app/features/org/partials/orgUsers.html +++ b/public/app/features/org/partials/orgUsers.html @@ -40,26 +40,32 @@ - - - - - - - - - - - -
EmailName
{{invite.email}}{{invite.name}} - -    - - - -
+
+ {{invite.email}} + {{invite.name}} + + +   + + + + + +
+ +   + + + Invited: {{invite.createdOn | date: 'shortDate'}} by {{invite.invitedBy}} + +
+
diff --git a/public/app/features/org/userInviteCtrl.js b/public/app/features/org/userInviteCtrl.js index 63f7fcc00a3..ad343af5bff 100644 --- a/public/app/features/org/userInviteCtrl.js +++ b/public/app/features/org/userInviteCtrl.js @@ -13,8 +13,8 @@ function (angular, _) { {name: '', email: '', role: 'Editor'}, ]; - $scope.init = function() { - }; + $scope.options = {skipEmails: false}; + $scope.init = function() { }; $scope.addInvite = function() { $scope.invites.push({name: '', email: '', role: 'Editor'}); @@ -28,6 +28,7 @@ function (angular, _) { if (!$scope.inviteForm.$valid) { return; } var promises = _.map($scope.invites, function(invite) { + invite.skipEmails = $scope.options.skipEmails; return backendSrv.post('/api/org/invites', invite); }); diff --git a/public/css/less/tables_lists.less b/public/css/less/tables_lists.less index 8582ec18f99..f0c7275ffe1 100644 --- a/public/css/less/tables_lists.less +++ b/public/css/less/tables_lists.less @@ -33,23 +33,11 @@ white-space: nowrap; } -.grafana-options-list { - list-style: none; - margin: 0; - max-width: 450px; - - li:nth-child(odd) { - background-color: @grafanaListAccent; - } - - li { - float: left; - margin: 2px; - padding: 5px 10px; - border: 1px solid @grafanaListBorderBottom; - border: 1px solid @grafanaListBorderBottom; - } - li:first-child { - border: 1px solid @grafanaListBorderBottom; - } +.grafana-list-item { + display: block; + padding: 1px 10px; + line-height: 34px; + background-color: @grafanaTargetBackground; + margin-bottom: 4px; + cursor: pointer; }