Bladeren bron

Add avatar to team and team members page (#10305)

* teams: add db migration for email column in teams table

* teams: /teams should render index page with a 200 OK

* teams: additional backend functionality for team and team members

Possibility to save/update email for teams.
Possibility to retrive avatar url when searching for teams.
Possibility to retrive avatar url when searching for team members.

* teams: display team avatar and team member avatars

Possibility to save and update email for a team

* teams: create team on separate page instead of modal dialog
Marcus Efraimsson 8 jaren geleden
bovenliggende
commit
af34f9977e

+ 3 - 0
pkg/api/api.go

@@ -40,8 +40,11 @@ func (hs *HttpServer) registerRoutes() {
 	r.Get("/datasources/", reqSignedIn, Index)
 	r.Get("/datasources/new", reqSignedIn, Index)
 	r.Get("/datasources/edit/*", reqSignedIn, Index)
+	r.Get("/org/users", reqSignedIn, Index)
 	r.Get("/org/users/new", reqSignedIn, Index)
 	r.Get("/org/users/invite", reqSignedIn, Index)
+	r.Get("/org/teams", reqSignedIn, Index)
+	r.Get("/org/teams/*", reqSignedIn, Index)
 	r.Get("/org/apikeys/", reqSignedIn, Index)
 	r.Get("/dashboard/import/", reqSignedIn, Index)
 	r.Get("/configuration", reqGrafanaAdmin, Index)

+ 17 - 0
pkg/api/dtos/models.go

@@ -3,6 +3,7 @@ package dtos
 import (
 	"crypto/md5"
 	"fmt"
+	"regexp"
 	"strings"
 
 	"github.com/grafana/grafana/pkg/components/simplejson"
@@ -57,3 +58,19 @@ func GetGravatarUrl(text string) string {
 	hasher.Write([]byte(strings.ToLower(text)))
 	return fmt.Sprintf(setting.AppSubUrl+"/avatar/%x", hasher.Sum(nil))
 }
+
+func GetGravatarUrlWithDefault(text string, defaultText string) string {
+	if text != "" {
+		return GetGravatarUrl(text)
+	}
+
+	reg, err := regexp.Compile("[^a-zA-Z0-9]+")
+
+	if err != nil {
+		return ""
+	}
+
+	text = reg.ReplaceAllString(defaultText, "") + "@localhost"
+
+	return GetGravatarUrl(text)
+}

+ 5 - 0
pkg/api/team.go

@@ -1,6 +1,7 @@
 package api
 
 import (
+	"github.com/grafana/grafana/pkg/api/dtos"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/middleware"
 	m "github.com/grafana/grafana/pkg/models"
@@ -70,6 +71,10 @@ func SearchTeams(c *middleware.Context) Response {
 		return ApiError(500, "Failed to search Teams", err)
 	}
 
+	for _, team := range query.Result.Teams {
+		team.AvatarUrl = dtos.GetGravatarUrlWithDefault(team.Email, team.Name)
+	}
+
 	query.Result.Page = page
 	query.Result.PerPage = perPage
 

+ 5 - 0
pkg/api/team_members.go

@@ -1,6 +1,7 @@
 package api
 
 import (
+	"github.com/grafana/grafana/pkg/api/dtos"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/middleware"
 	m "github.com/grafana/grafana/pkg/models"
@@ -15,6 +16,10 @@ func GetTeamMembers(c *middleware.Context) Response {
 		return ApiError(500, "Failed to get Team Members", err)
 	}
 
+	for _, member := range query.Result {
+		member.AvatarUrl = dtos.GetGravatarUrl(member.Email)
+	}
+
 	return Json(200, query.Result)
 }
 

+ 7 - 2
pkg/models/team.go

@@ -16,6 +16,7 @@ type Team struct {
 	Id    int64  `json:"id"`
 	OrgId int64  `json:"orgId"`
 	Name  string `json:"name"`
+	Email string `json:"email"`
 
 	Created time.Time `json:"created"`
 	Updated time.Time `json:"updated"`
@@ -26,14 +27,16 @@ type Team struct {
 
 type CreateTeamCommand struct {
 	Name  string `json:"name" binding:"Required"`
+	Email string `json:"email"`
 	OrgId int64  `json:"-"`
 
 	Result Team `json:"-"`
 }
 
 type UpdateTeamCommand struct {
-	Id   int64
-	Name string
+	Id    int64
+	Name  string
+	Email string
 }
 
 type DeleteTeamCommand struct {
@@ -64,6 +67,8 @@ type SearchTeamDto struct {
 	Id          int64  `json:"id"`
 	OrgId       int64  `json:"orgId"`
 	Name        string `json:"name"`
+	Email       string `json:"email"`
+	AvatarUrl   string `json:"avatarUrl"`
 	MemberCount int64  `json:"memberCount"`
 }
 

+ 6 - 5
pkg/models/team_member.go

@@ -47,9 +47,10 @@ type GetTeamMembersQuery struct {
 // Projections and DTOs
 
 type TeamMemberDTO struct {
-	OrgId  int64  `json:"orgId"`
-	TeamId int64  `json:"teamId"`
-	UserId int64  `json:"userId"`
-	Email  string `json:"email"`
-	Login  string `json:"login"`
+	OrgId     int64  `json:"orgId"`
+	TeamId    int64  `json:"teamId"`
+	UserId    int64  `json:"userId"`
+	Email     string `json:"email"`
+	Login     string `json:"login"`
+	AvatarUrl string `json:"avatarUrl"`
 }

+ 5 - 0
pkg/services/sqlstore/migrations/team_mig.go

@@ -45,4 +45,9 @@ func addTeamMigrations(mg *Migrator) {
 	//-------  indexes ------------------
 	mg.AddMigration("add index team_member.org_id", NewAddIndexMigration(teamMemberV1, teamMemberV1.Indices[0]))
 	mg.AddMigration("add unique index team_member_org_id_team_id_user_id", NewAddIndexMigration(teamMemberV1, teamMemberV1.Indices[1]))
+
+	// add column email
+	mg.AddMigration("Add column email to team table", NewAddColumnMigration(teamV1, &Column{
+		Name: "email", Type: DB_NVarchar, Nullable: true, Length: 190,
+	}))
 }

+ 5 - 0
pkg/services/sqlstore/team.go

@@ -33,6 +33,7 @@ func CreateTeam(cmd *m.CreateTeamCommand) error {
 
 		team := m.Team{
 			Name:    cmd.Name,
+			Email:   cmd.Email,
 			OrgId:   cmd.OrgId,
 			Created: time.Now(),
 			Updated: time.Now(),
@@ -57,9 +58,12 @@ func UpdateTeam(cmd *m.UpdateTeamCommand) error {
 
 		team := m.Team{
 			Name:    cmd.Name,
+			Email:   cmd.Email,
 			Updated: time.Now(),
 		}
 
+		sess.MustCols("email")
+
 		affectedRows, err := sess.Id(cmd.Id).Update(&team)
 
 		if err != nil {
@@ -125,6 +129,7 @@ func SearchTeams(query *m.SearchTeamsQuery) error {
 	sql.WriteString(`select
 		team.id as id,
 		team.name as name,
+		team.email as email,
 		(select count(*) from team_member where team_member.team_id = team.id) as member_count
 		from team as team
 		where team.org_id = ?`)

+ 4 - 2
pkg/services/sqlstore/team_test.go

@@ -27,8 +27,8 @@ func TestTeamCommandsAndQueries(t *testing.T) {
 				userIds = append(userIds, userCmd.Result.Id)
 			}
 
-			group1 := m.CreateTeamCommand{Name: "group1 name"}
-			group2 := m.CreateTeamCommand{Name: "group2 name"}
+			group1 := m.CreateTeamCommand{Name: "group1 name", Email: "test1@test.com"}
+			group2 := m.CreateTeamCommand{Name: "group2 name", Email: "test2@test.com"}
 
 			err := CreateTeam(&group1)
 			So(err, ShouldBeNil)
@@ -43,6 +43,7 @@ func TestTeamCommandsAndQueries(t *testing.T) {
 
 				team1 := query.Result.Teams[0]
 				So(team1.Name, ShouldEqual, "group1 name")
+				So(team1.Email, ShouldEqual, "test1@test.com")
 
 				err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: 1, TeamId: team1.Id, UserId: userIds[0]})
 				So(err, ShouldBeNil)
@@ -76,6 +77,7 @@ func TestTeamCommandsAndQueries(t *testing.T) {
 				So(err, ShouldBeNil)
 				So(len(query.Result), ShouldEqual, 1)
 				So(query.Result[0].Name, ShouldEqual, "group2 name")
+				So(query.Result[0].Email, ShouldEqual, "test2@test.com")
 			})
 
 			Convey("Should be able to remove users from a group", func() {

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

@@ -145,6 +145,12 @@ function setupAngularRoutes($routeProvider, $locationProvider) {
       controllerAs: 'ctrl',
       resolve: loadOrgBundle,
     })
+    .when('/org/teams/new', {
+      templateUrl: 'public/app/features/org/partials/create_team.html',
+      controller: 'CreateTeamCtrl',
+      controllerAs: 'ctrl',
+      resolve: loadOrgBundle,
+    })
     .when('/org/teams/edit/:id', {
       templateUrl: 'public/app/features/org/partials/team_details.html',
       controller: 'TeamDetailsCtrl',

+ 13 - 13
public/app/features/org/all.ts

@@ -1,13 +1,13 @@
-import './org_users_ctrl';
-import './profile_ctrl';
-import './org_users_ctrl';
-import './select_org_ctrl';
-import './change_password_ctrl';
-import './new_org_ctrl';
-import './user_invite_ctrl';
-import './teams_ctrl';
-import './team_details_ctrl';
-import './create_team_modal';
-import './org_api_keys_ctrl';
-import './org_details_ctrl';
-import './prefs_control';
+import "./org_users_ctrl";
+import "./profile_ctrl";
+import "./org_users_ctrl";
+import "./select_org_ctrl";
+import "./change_password_ctrl";
+import "./new_org_ctrl";
+import "./user_invite_ctrl";
+import "./teams_ctrl";
+import "./team_details_ctrl";
+import "./create_team_ctrl";
+import "./org_api_keys_ctrl";
+import "./org_details_ctrl";
+import "./prefs_control";

+ 26 - 0
public/app/features/org/create_team_ctrl.ts

@@ -0,0 +1,26 @@
+import coreModule from "app/core/core_module";
+
+export default class CreateTeamCtrl {
+  name: string;
+  email: string;
+  navModel: any;
+
+  /** @ngInject **/
+  constructor(private backendSrv, private $location, navModelSrv) {
+    this.navModel = navModelSrv.getNav("cfg", "teams", 0);
+  }
+
+  create() {
+    const payload = {
+      name: this.name,
+      email: this.email
+    };
+    this.backendSrv.post("/api/teams", payload).then(result => {
+      if (result.teamId) {
+        this.$location.path("/org/teams/edit/" + result.teamId);
+      }
+    });
+  }
+}
+
+coreModule.controller("CreateTeamCtrl", CreateTeamCtrl);

+ 0 - 36
public/app/features/org/create_team_modal.ts

@@ -1,36 +0,0 @@
-///<reference path="../../headers/common.d.ts" />
-
-import coreModule from 'app/core/core_module';
-import appEvents from 'app/core/app_events';
-
-export class CreateTeamCtrl {
-  teamName = '';
-
-  /** @ngInject */
-  constructor(private backendSrv, private $location) {}
-
-  createTeam() {
-    this.backendSrv.post('/api/teams', { name: this.teamName }).then(result => {
-      if (result.teamId) {
-        this.$location.path('/org/teams/edit/' + result.teamId);
-      }
-      this.dismiss();
-    });
-  }
-
-  dismiss() {
-    appEvents.emit('hide-modal');
-  }
-}
-
-export function createTeamModal() {
-  return {
-    restrict: 'E',
-    templateUrl: 'public/app/features/org/partials/create_team.html',
-    controller: CreateTeamCtrl,
-    bindToController: true,
-    controllerAs: 'ctrl',
-  };
-}
-
-coreModule.directive('createTeamModal', createTeamModal);

+ 23 - 24
public/app/features/org/partials/create_team.html

@@ -1,27 +1,26 @@
-<div class="modal-body">
-  <div class="modal-header">
-		<h2 class="modal-header-title">
-			<i class="gicon gicon-team"></i>
-			<span class="p-l-1">Create Team</span>
-		</h2>
+<page-header model="ctrl.navModel"></page-header>
 
-		<a class="modal-header-close" ng-click="ctrl.dismiss();">
-			<i class="fa fa-remove"></i>
-		</a>
-	</div>
+<div class="page-container page-body" ng-cloak>
+	<h3 class="page-sub-heading">New Team</h3>
 
-	<div class="modal-content">
-		<form name="ctrl.createTeamForm" class="gf-form-group" novalidate>
-      <div class="p-t-2">
-        <div class="gf-form-inline">
-          <div class="gf-form max-width-21">
-            <input type="text" class="gf-form-input" ng-model='ctrl.teamName' required give-focus="true" placeholder="Enter Team Name"></input>
-          </div>
-          <div class="gf-form">
-            <button class="btn gf-form-btn btn-success" ng-click="ctrl.createTeam();ctrl.dismiss();">Create</button>
-          </div>
-        </div>
-      </div>
-		</form>
-	</div>
+	<form name="ctrl.saveForm" class="gf-form-group" ng-submit="ctrl.create()">
+		<div class="gf-form max-width-30">
+			<span class="gf-form-label width-10">Name</span>
+			<input type="text" required ng-model="ctrl.name" class="gf-form-input max-width-22" give-focus="true">
+		</div>
+		<div class="gf-form max-width-30">
+			<span class="gf-form-label width-10">
+				Email
+				<info-popover mode="right-normal">
+					This is optional and is primarily used for allowing custom team avatars.
+				</info-popover>
+			</span>
+			<input class="gf-form-input max-width-22" type="email" ng-model="ctrl.email" placeholder="email@test.com">
+		</div>
+		<div class="gf-form-button-row">
+			<button type="submit" class="btn btn-success width-12">
+				<i class="fa fa-save"></i> Create
+			</button>
+		</div>
+	</form>
 </div>

+ 18 - 5
public/app/features/org/partials/team_details.html

@@ -3,13 +3,22 @@
 <div class="page-container page-body">
 	<h3 class="page-sub-heading">Team Details</h3>
 
-  <form name="teamDetailsForm" class="gf-form-group gf-form-inline">
-    <div class="gf-form">
+  <form name="teamDetailsForm" class="gf-form-group">
+    <div class="gf-form max-width-30">
       <span class="gf-form-label width-10">Name</span>
-      <input type="text" required ng-model="ctrl.team.name" class="gf-form-input max-width-14">
-    </div>
+      <input type="text" required ng-model="ctrl.team.name" class="gf-form-input max-width-22">
+		</div>
+		<div class="gf-form max-width-30">
+			<span class="gf-form-label width-10">
+				Email
+				<info-popover mode="right-normal">
+						This is optional and is primarily used for allowing custom team avatars.
+				</info-popover>
+			</span>
+			<input class="gf-form-input max-width-22" type="email" ng-model="ctrl.team.email" placeholder="email@test.com">
+		</div>
 
-    <div class="gf-form">
+    <div class="gf-form-button-row">
       <button type="submit" class="btn btn-success" ng-click="ctrl.update()">Update</button>
     </div>
   </form>
@@ -28,12 +37,16 @@
     <table class="filter-table" ng-show="ctrl.teamMembers.length > 0">
 			<thead>
 				<tr>
+					<th></th>
 					<th>Username</th>
 					<th>Email</th>
 					<th></th>
 				</tr>
 			</thead>
 			<tr ng-repeat="member in ctrl.teamMembers">
+				<td class="width-4 text-center link-td">
+					<img class="filter-table__avatar" ng-src="{{member.avatarUrl}}"></img>
+				</td>
 				<td>{{member.login}}</td>
 				<td>{{member.email}}</td>
 				<td style="width: 1%">

+ 11 - 1
public/app/features/org/partials/teams.html

@@ -8,7 +8,7 @@
     </label>
     <div class="page-action-bar__spacer"></div>
 
-    <a class="btn btn-success" ng-click="ctrl.openTeamModal()">
+    <a class="btn btn-success" href="/org/teams/new">
       <i class="fa fa-plus"></i>
 			Add Team
     </a>
@@ -18,16 +18,26 @@
     <table class="filter-table filter-table--hover form-inline" ng-show="ctrl.teams.length > 0">
       <thead>
         <tr>
+          <th></th>
           <th>Name</th>
+          <th>Email</th>
           <th>Members</th>
           <th style="width: 1%"></th>
         </tr>
       </thead>
       <tbody>
         <tr ng-repeat="team in ctrl.teams">
+          <td class="width-4 text-center link-td">
+            <a href="org/teams/edit/{{team.id}}">
+              <img class="filter-table__avatar" ng-src="{{team.avatarUrl}}"></img>
+            </a>
+          </td>
           <td class="link-td">
             <a href="org/teams/edit/{{team.id}}">{{team.name}}</a>
           </td>
+          <td class="link-td">
+              <a href="org/teams/edit/{{team.id}}">{{team.email}}</a>
+            </td>
           <td class="link-td">
             <a href="org/teams/edit/{{team.id}}">{{team.memberCount}}</a>
           </td>

+ 5 - 1
public/app/features/org/team_details_ctrl.ts

@@ -55,7 +55,10 @@ export default class TeamDetailsCtrl {
       return;
     }
 
-    this.backendSrv.put('/api/teams/' + this.team.id, { name: this.team.name });
+    this.backendSrv.put('/api/teams/' + this.team.id, {
+      name: this.team.name,
+      email: this.team.email,
+    });
   }
 
   userPicked(user) {
@@ -71,6 +74,7 @@ export default class TeamDetailsCtrl {
 export interface Team {
   id: number;
   name: string;
+  email: string;
 }
 
 export interface User {