Commit af34f997 by Marcus Efraimsson Committed by Torkel Ödegaard

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
parent d41ce4f9
......@@ -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)
......
......@@ -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)
}
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
......
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)
}
......
......@@ -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"`
}
......
......@@ -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"`
}
......@@ -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,
}))
}
......@@ -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 = ?`)
......
......@@ -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() {
......
......@@ -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',
......
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";
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);
///<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);
<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>
......@@ -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%">
......
......@@ -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,17 +18,27 @@
<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>
<td class="text-right">
......
......@@ -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 {
......
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