mirror of
https://github.com/grafana/grafana.git
synced 2024-12-28 01:41:24 -06:00
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
This commit is contained in:
parent
d41ce4f9ca
commit
af34f9977e
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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
public/app/features/org/create_team_ctrl.ts
Normal file
26
public/app/features/org/create_team_ctrl.ts
Normal file
@ -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);
|
@ -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);
|
@ -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>
|
||||
|
@ -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,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>
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user