Commit 668cb3c1 by Torkel Ödegaard

Merge branch 'v4.4.x'

parents eac0d30e e8a20643
...@@ -67,10 +67,10 @@ func updateTotalStats() { ...@@ -67,10 +67,10 @@ func updateTotalStats() {
return return
} }
M_StatTotal_Dashboards.Update(statsQuery.Result.DashboardCount) M_StatTotal_Dashboards.Update(statsQuery.Result.Dashboards)
M_StatTotal_Users.Update(statsQuery.Result.UserCount) M_StatTotal_Users.Update(statsQuery.Result.Users)
M_StatTotal_Playlists.Update(statsQuery.Result.PlaylistCount) M_StatTotal_Playlists.Update(statsQuery.Result.Playlists)
M_StatTotal_Orgs.Update(statsQuery.Result.OrgCount) M_StatTotal_Orgs.Update(statsQuery.Result.Orgs)
} }
} }
...@@ -97,14 +97,16 @@ func sendUsageStats() { ...@@ -97,14 +97,16 @@ func sendUsageStats() {
return return
} }
metrics["stats.dashboards.count"] = statsQuery.Result.DashboardCount metrics["stats.dashboards.count"] = statsQuery.Result.Dashboards
metrics["stats.users.count"] = statsQuery.Result.UserCount metrics["stats.users.count"] = statsQuery.Result.Users
metrics["stats.orgs.count"] = statsQuery.Result.OrgCount metrics["stats.orgs.count"] = statsQuery.Result.Orgs
metrics["stats.playlist.count"] = statsQuery.Result.PlaylistCount metrics["stats.playlist.count"] = statsQuery.Result.Playlists
metrics["stats.plugins.apps.count"] = len(plugins.Apps) metrics["stats.plugins.apps.count"] = len(plugins.Apps)
metrics["stats.plugins.panels.count"] = len(plugins.Panels) metrics["stats.plugins.panels.count"] = len(plugins.Panels)
metrics["stats.plugins.datasources.count"] = len(plugins.DataSources) metrics["stats.plugins.datasources.count"] = len(plugins.DataSources)
metrics["stats.alerts.count"] = statsQuery.Result.AlertCount metrics["stats.alerts.count"] = statsQuery.Result.Alerts
metrics["stats.active_users.count"] = statsQuery.Result.ActiveUsers
metrics["stats.datasources.count"] = statsQuery.Result.Datasources
dsStats := m.GetDataSourceStatsQuery{} dsStats := m.GetDataSourceStatsQuery{}
if err := bus.Dispatch(&dsStats); err != nil { if err := bus.Dispatch(&dsStats); err != nil {
......
...@@ -62,6 +62,15 @@ func GetContextHandler() macaron.Handler { ...@@ -62,6 +62,15 @@ func GetContextHandler() macaron.Handler {
ctx.Data["ctx"] = ctx ctx.Data["ctx"] = ctx
c.Map(ctx) c.Map(ctx)
// update last seen at
// update last seen every 5min
if ctx.ShouldUpdateLastSeenAt() {
ctx.Logger.Debug("Updating last user_seen_at", "user_id", ctx.UserId)
if err := bus.Dispatch(&m.UpdateUserLastSeenAtCommand{UserId: ctx.UserId}); err != nil {
ctx.Logger.Error("Failed to update last_seen_at", "error", err)
}
}
} }
} }
...@@ -99,7 +108,7 @@ func initContextWithUserSessionCookie(ctx *Context, orgId int64) bool { ...@@ -99,7 +108,7 @@ func initContextWithUserSessionCookie(ctx *Context, orgId int64) bool {
query := m.GetSignedInUserQuery{UserId: userId, OrgId: orgId} query := m.GetSignedInUserQuery{UserId: userId, OrgId: orgId}
if err := bus.Dispatch(&query); err != nil { if err := bus.Dispatch(&query); err != nil {
ctx.Logger.Error("Failed to get user with id", "userId", userId) ctx.Logger.Error("Failed to get user with id", "userId", userId, "error", err)
return false return false
} }
......
...@@ -108,4 +108,6 @@ type OrgUserDTO struct { ...@@ -108,4 +108,6 @@ type OrgUserDTO struct {
Email string `json:"email"` Email string `json:"email"`
Login string `json:"login"` Login string `json:"login"`
Role string `json:"role"` Role string `json:"role"`
LastSeenAt time.Time `json:"lastSeenAt"`
LastSeenAtAge string `json:"lastSeenAtAge"`
} }
package models package models
type SystemStats struct { type SystemStats struct {
DashboardCount int64 Dashboards int64
UserCount int64 Datasources int64
OrgCount int64 Users int64
PlaylistCount int64 ActiveUsers int64
AlertCount int64 Orgs int64
Playlists int64
Alerts int64
} }
type DataSourceStats struct { type DataSourceStats struct {
...@@ -22,15 +24,16 @@ type GetDataSourceStatsQuery struct { ...@@ -22,15 +24,16 @@ type GetDataSourceStatsQuery struct {
} }
type AdminStats struct { type AdminStats struct {
UserCount int `json:"user_count"` Users int `json:"users"`
OrgCount int `json:"org_count"` Orgs int `json:"orgs"`
DashboardCount int `json:"dashboard_count"` Dashboards int `json:"dashboards"`
DbSnapshotCount int `json:"db_snapshot_count"` Snapshots int `json:"snapshots"`
DbTagCount int `json:"db_tag_count"` Tags int `json:"tags"`
DataSourceCount int `json:"data_source_count"` Datasources int `json:"datasources"`
PlaylistCount int `json:"playlist_count"` Playlists int `json:"playlists"`
StarredDbCount int `json:"starred_db_count"` Stars int `json:"stars"`
AlertCount int `json:"alert_count"` Alerts int `json:"alerts"`
ActiveUsers int `json:"activeUsers"`
} }
type GetAdminStatsQuery struct { type GetAdminStatsQuery struct {
......
...@@ -35,6 +35,7 @@ type User struct { ...@@ -35,6 +35,7 @@ type User struct {
Created time.Time Created time.Time
Updated time.Time Updated time.Time
LastSeenAt time.Time
} }
func (u *User) NameOrFallback() string { func (u *User) NameOrFallback() string {
...@@ -127,6 +128,7 @@ type GetUserProfileQuery struct { ...@@ -127,6 +128,7 @@ type GetUserProfileQuery struct {
} }
type SearchUsersQuery struct { type SearchUsersQuery struct {
OrgId int64
Query string Query string
Page int Page int
Limit int Limit int
...@@ -160,6 +162,15 @@ type SignedInUser struct { ...@@ -160,6 +162,15 @@ type SignedInUser struct {
ApiKeyId int64 ApiKeyId int64
IsGrafanaAdmin bool IsGrafanaAdmin bool
HelpFlags1 HelpFlags1 HelpFlags1 HelpFlags1
LastSeenAt time.Time
}
func (u *SignedInUser) ShouldUpdateLastSeenAt() bool {
return u.UserId > 0 && time.Since(u.LastSeenAt) > time.Minute*5
}
type UpdateUserLastSeenAtCommand struct {
UserId int64
} }
type UserProfileDTO struct { type UserProfileDTO struct {
...@@ -178,6 +189,8 @@ type UserSearchHitDTO struct { ...@@ -178,6 +189,8 @@ type UserSearchHitDTO struct {
Login string `json:"login"` Login string `json:"login"`
Email string `json:"email"` Email string `json:"email"`
IsAdmin bool `json:"isAdmin"` IsAdmin bool `json:"isAdmin"`
LastSeenAt time.Time `json:"lastSeenAt"`
LastSeenAtAge string `json:"lastSeenAtAge"`
} }
type UserIdDTO struct { type UserIdDTO struct {
......
...@@ -103,4 +103,8 @@ func addUserMigrations(mg *Migrator) { ...@@ -103,4 +103,8 @@ func addUserMigrations(mg *Migrator) {
{Name: "company", Type: DB_NVarchar, Length: 255, Nullable: true}, {Name: "company", Type: DB_NVarchar, Length: 255, Nullable: true},
{Name: "theme", Type: DB_NVarchar, Length: 255, Nullable: true}, {Name: "theme", Type: DB_NVarchar, Length: 255, Nullable: true},
})) }))
mg.AddMigration("Add last_seen_at column to user", NewAddColumnMigration(userV2, &Column{
Name: "last_seen_at", Type: DB_DateTime, Nullable: true,
}))
} }
...@@ -6,6 +6,7 @@ import ( ...@@ -6,6 +6,7 @@ import (
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/util"
) )
func init() { func init() {
...@@ -71,11 +72,18 @@ func GetOrgUsers(query *m.GetOrgUsersQuery) error { ...@@ -71,11 +72,18 @@ func GetOrgUsers(query *m.GetOrgUsersQuery) error {
sess := x.Table("org_user") sess := x.Table("org_user")
sess.Join("INNER", "user", fmt.Sprintf("org_user.user_id=%s.id", x.Dialect().Quote("user"))) sess.Join("INNER", "user", fmt.Sprintf("org_user.user_id=%s.id", x.Dialect().Quote("user")))
sess.Where("org_user.org_id=?", query.OrgId) sess.Where("org_user.org_id=?", query.OrgId)
sess.Cols("org_user.org_id", "org_user.user_id", "user.email", "user.login", "org_user.role") sess.Cols("org_user.org_id", "org_user.user_id", "user.email", "user.login", "org_user.role", "user.last_seen_at")
sess.Asc("user.email", "user.login") sess.Asc("user.email", "user.login")
err := sess.Find(&query.Result) if err := sess.Find(&query.Result); err != nil {
return err return err
}
for _, user := range query.Result {
user.LastSeenAtAge = util.GetAgeString(user.LastSeenAt)
}
return nil
} }
func RemoveOrgUser(cmd *m.RemoveOrgUserCommand) error { func RemoveOrgUser(cmd *m.RemoveOrgUserCommand) error {
......
...@@ -53,7 +53,7 @@ func EnsureAdminUser() { ...@@ -53,7 +53,7 @@ func EnsureAdminUser() {
return return
} }
if statsQuery.Result.UserCount > 0 { if statsQuery.Result.Users > 0 {
return return
} }
......
package sqlstore package sqlstore
import ( import (
"time"
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
) )
...@@ -11,6 +13,8 @@ func init() { ...@@ -11,6 +13,8 @@ func init() {
bus.AddHandler("sql", GetAdminStats) bus.AddHandler("sql", GetAdminStats)
} }
var activeUserTimeLimit time.Duration = time.Hour * 24 * 14
func GetDataSourceStats(query *m.GetDataSourceStatsQuery) error { func GetDataSourceStats(query *m.GetDataSourceStatsQuery) error {
var rawSql = `SELECT COUNT(*) as count, type FROM data_source GROUP BY type` var rawSql = `SELECT COUNT(*) as count, type FROM data_source GROUP BY type`
query.Result = make([]*m.DataSourceStats, 0) query.Result = make([]*m.DataSourceStats, 0)
...@@ -27,27 +31,35 @@ func GetSystemStats(query *m.GetSystemStatsQuery) error { ...@@ -27,27 +31,35 @@ func GetSystemStats(query *m.GetSystemStatsQuery) error {
( (
SELECT COUNT(*) SELECT COUNT(*)
FROM ` + dialect.Quote("user") + ` FROM ` + dialect.Quote("user") + `
) AS user_count, ) AS users,
( (
SELECT COUNT(*) SELECT COUNT(*)
FROM ` + dialect.Quote("org") + ` FROM ` + dialect.Quote("org") + `
) AS org_count, ) AS orgs,
( (
SELECT COUNT(*) SELECT COUNT(*)
FROM ` + dialect.Quote("dashboard") + ` FROM ` + dialect.Quote("dashboard") + `
) AS dashboard_count, ) AS dashboards,
(
SELECT COUNT(*)
FROM ` + dialect.Quote("data_source") + `
) AS datasources,
( (
SELECT COUNT(*) SELECT COUNT(*)
FROM ` + dialect.Quote("playlist") + ` FROM ` + dialect.Quote("playlist") + `
) AS playlist_count, ) AS playlists,
( (
SELECT COUNT(*) SELECT COUNT(*)
FROM ` + dialect.Quote("alert") + ` FROM ` + dialect.Quote("alert") + `
) AS alert_count ) AS alerts,
(
SELECT COUNT(*) FROM ` + dialect.Quote("user") + ` where last_seen_at > ?
) as active_users
` `
activeUserDeadlineDate := time.Now().Add(-activeUserTimeLimit)
var stats m.SystemStats var stats m.SystemStats
_, err := x.Sql(rawSql).Get(&stats) _, err := x.Sql(rawSql, activeUserDeadlineDate).Get(&stats)
if err != nil { if err != nil {
return err return err
} }
...@@ -61,43 +73,48 @@ func GetAdminStats(query *m.GetAdminStatsQuery) error { ...@@ -61,43 +73,48 @@ func GetAdminStats(query *m.GetAdminStatsQuery) error {
( (
SELECT COUNT(*) SELECT COUNT(*)
FROM ` + dialect.Quote("user") + ` FROM ` + dialect.Quote("user") + `
) AS user_count, ) AS users,
( (
SELECT COUNT(*) SELECT COUNT(*)
FROM ` + dialect.Quote("org") + ` FROM ` + dialect.Quote("org") + `
) AS org_count, ) AS orgs,
( (
SELECT COUNT(*) SELECT COUNT(*)
FROM ` + dialect.Quote("dashboard") + ` FROM ` + dialect.Quote("dashboard") + `
) AS dashboard_count, ) AS dashboards,
( (
SELECT COUNT(*) SELECT COUNT(*)
FROM ` + dialect.Quote("dashboard_snapshot") + ` FROM ` + dialect.Quote("dashboard_snapshot") + `
) AS db_snapshot_count, ) AS snapshots,
( (
SELECT COUNT( DISTINCT ( ` + dialect.Quote("term") + ` )) SELECT COUNT( DISTINCT ( ` + dialect.Quote("term") + ` ))
FROM ` + dialect.Quote("dashboard_tag") + ` FROM ` + dialect.Quote("dashboard_tag") + `
) AS db_tag_count, ) AS tags,
( (
SELECT COUNT(*) SELECT COUNT(*)
FROM ` + dialect.Quote("data_source") + ` FROM ` + dialect.Quote("data_source") + `
) AS data_source_count, ) AS datasources,
( (
SELECT COUNT(*) SELECT COUNT(*)
FROM ` + dialect.Quote("playlist") + ` FROM ` + dialect.Quote("playlist") + `
) AS playlist_count, ) AS playlists,
( (
SELECT COUNT(DISTINCT ` + dialect.Quote("dashboard_id") + ` ) SELECT COUNT(*) FROM ` + dialect.Quote("star") + `
FROM ` + dialect.Quote("star") + ` ) AS stars,
) AS starred_db_count,
( (
SELECT COUNT(*) SELECT COUNT(*)
FROM ` + dialect.Quote("alert") + ` FROM ` + dialect.Quote("alert") + `
) AS alert_count ) AS alerts,
(
SELECT COUNT(*)
from ` + dialect.Quote("user") + ` where last_seen_at > ?
) as active_users
` `
activeUserDeadlineDate := time.Now().Add(-activeUserTimeLimit)
var stats m.AdminStats var stats m.AdminStats
_, err := x.Sql(rawSql).Get(&stats) _, err := x.Sql(rawSql, activeUserDeadlineDate).Get(&stats)
if err != nil { if err != nil {
return err return err
} }
......
...@@ -22,6 +22,7 @@ func init() { ...@@ -22,6 +22,7 @@ func init() {
bus.AddHandler("sql", GetUserByLogin) bus.AddHandler("sql", GetUserByLogin)
bus.AddHandler("sql", GetUserByEmail) bus.AddHandler("sql", GetUserByEmail)
bus.AddHandler("sql", SetUsingOrg) bus.AddHandler("sql", SetUsingOrg)
bus.AddHandler("sql", UpdateUserLastSeenAt)
bus.AddHandler("sql", GetUserProfile) bus.AddHandler("sql", GetUserProfile)
bus.AddHandler("sql", GetSignedInUser) bus.AddHandler("sql", GetSignedInUser)
bus.AddHandler("sql", SearchUsers) bus.AddHandler("sql", SearchUsers)
...@@ -260,6 +261,24 @@ func ChangeUserPassword(cmd *m.ChangeUserPasswordCommand) error { ...@@ -260,6 +261,24 @@ func ChangeUserPassword(cmd *m.ChangeUserPasswordCommand) error {
}) })
} }
func UpdateUserLastSeenAt(cmd *m.UpdateUserLastSeenAtCommand) error {
return inTransaction(func(sess *DBSession) error {
if cmd.UserId <= 0 {
}
user := m.User{
Id: cmd.UserId,
LastSeenAt: time.Now(),
}
if _, err := sess.Id(cmd.UserId).Update(&user); err != nil {
return err
}
return nil
})
}
func SetUsingOrg(cmd *m.SetUsingOrgCommand) error { func SetUsingOrg(cmd *m.SetUsingOrgCommand) error {
getOrgsForUserCmd := &m.GetUserOrgListQuery{UserId: cmd.UserId} getOrgsForUserCmd := &m.GetUserOrgListQuery{UserId: cmd.UserId}
GetUserOrgList(getOrgsForUserCmd) GetUserOrgList(getOrgsForUserCmd)
...@@ -330,6 +349,7 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error { ...@@ -330,6 +349,7 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error {
u.login as login, u.login as login,
u.name as name, u.name as name,
u.help_flags1 as help_flags1, u.help_flags1 as help_flags1,
u.last_seen_at as last_seen_at,
org.name as org_name, org.name as org_name,
org_user.role as org_role, org_user.role as org_role,
org.id as org_id org.id as org_id
...@@ -367,27 +387,49 @@ func SearchUsers(query *m.SearchUsersQuery) error { ...@@ -367,27 +387,49 @@ func SearchUsers(query *m.SearchUsersQuery) error {
query.Result = m.SearchUserQueryResult{ query.Result = m.SearchUserQueryResult{
Users: make([]*m.UserSearchHitDTO, 0), Users: make([]*m.UserSearchHitDTO, 0),
} }
queryWithWildcards := "%" + query.Query + "%" queryWithWildcards := "%" + query.Query + "%"
whereConditions := make([]string, 0)
whereParams := make([]interface{}, 0)
sess := x.Table("user") sess := x.Table("user")
if query.OrgId > 0 {
whereConditions = append(whereConditions, "org_id = ?")
whereParams = append(whereParams, query.OrgId)
}
if query.Query != "" { if query.Query != "" {
sess.Where("email LIKE ? OR name LIKE ? OR login like ?", queryWithWildcards, queryWithWildcards, queryWithWildcards) whereConditions = append(whereConditions, "(email LIKE ? OR name LIKE ? OR login like ?)")
whereParams = append(whereParams, queryWithWildcards, queryWithWildcards, queryWithWildcards)
}
if len(whereConditions) > 0 {
sess.Where(strings.Join(whereConditions, " AND "), whereParams...)
} }
offset := query.Limit * (query.Page - 1) offset := query.Limit * (query.Page - 1)
sess.Limit(query.Limit, offset) sess.Limit(query.Limit, offset)
sess.Cols("id", "email", "name", "login", "is_admin") sess.Cols("id", "email", "name", "login", "is_admin", "last_seen_at")
if err := sess.Find(&query.Result.Users); err != nil { if err := sess.Find(&query.Result.Users); err != nil {
return err return err
} }
// get total
user := m.User{} user := m.User{}
countSess := x.Table("user") countSess := x.Table("user")
if query.Query != "" {
countSess.Where("email LIKE ? OR name LIKE ? OR login like ?", queryWithWildcards, queryWithWildcards, queryWithWildcards) if len(whereConditions) > 0 {
countSess.Where(strings.Join(whereConditions, " AND "), whereParams...)
} }
count, err := countSess.Count(&user) count, err := countSess.Count(&user)
query.Result.TotalCount = count query.Result.TotalCount = count
for _, user := range query.Result.Users {
user.LastSeenAtAge = util.GetAgeString(user.LastSeenAt)
}
return err return err
} }
......
package util package util
import ( import (
"fmt"
"math"
"regexp" "regexp"
"time"
) )
func StringsFallback2(val1 string, val2 string) string { func StringsFallback2(val1 string, val2 string) string {
...@@ -28,3 +31,34 @@ func SplitString(str string) []string { ...@@ -28,3 +31,34 @@ func SplitString(str string) []string {
return regexp.MustCompile("[, ]+").Split(str, -1) return regexp.MustCompile("[, ]+").Split(str, -1)
} }
func GetAgeString(t time.Time) string {
if t.IsZero() {
return "?"
}
sinceNow := time.Since(t)
minutes := sinceNow.Minutes()
years := int(math.Floor(minutes / 525600))
months := int(math.Floor(minutes / 43800))
days := int(math.Floor(minutes / 1440))
hours := int(math.Floor(minutes / 60))
if years > 0 {
return fmt.Sprintf("%dy", years)
}
if months > 0 {
return fmt.Sprintf("%dM", months)
}
if days > 0 {
return fmt.Sprintf("%dd", days)
}
if hours > 0 {
return fmt.Sprintf("%dh", hours)
}
if int(minutes) > 0 {
return fmt.Sprintf("%dm", int(minutes))
}
return "< 1m"
}
...@@ -2,6 +2,7 @@ package util ...@@ -2,6 +2,7 @@ package util
import ( import (
"testing" "testing"
"time"
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
) )
...@@ -24,3 +25,15 @@ func TestSplitString(t *testing.T) { ...@@ -24,3 +25,15 @@ func TestSplitString(t *testing.T) {
So(SplitString("test1 , test2 test3"), ShouldResemble, []string{"test1", "test2", "test3"}) So(SplitString("test1 , test2 test3"), ShouldResemble, []string{"test1", "test2", "test3"})
}) })
} }
func TestDateAge(t *testing.T) {
Convey("GetAgeString", t, func() {
So(GetAgeString(time.Time{}), ShouldEqual, "?")
So(GetAgeString(time.Now().Add(-time.Second*2)), ShouldEqual, "< 1m")
So(GetAgeString(time.Now().Add(-time.Minute*2)), ShouldEqual, "2m")
So(GetAgeString(time.Now().Add(-time.Hour*2)), ShouldEqual, "2h")
So(GetAgeString(time.Now().Add(-time.Hour*24*3)), ShouldEqual, "3d")
So(GetAgeString(time.Now().Add(-time.Hour*24*67)), ShouldEqual, "2M")
So(GetAgeString(time.Now().Add(-time.Hour*24*409)), ShouldEqual, "1y")
})
}
...@@ -15,39 +15,43 @@ ...@@ -15,39 +15,43 @@
<tbody> <tbody>
<tr> <tr>
<td>Total dashboards</td> <td>Total dashboards</td>
<td>{{ctrl.stats.dashboard_count}}</td> <td>{{ctrl.stats.dashboards}}</td>
</tr> </tr>
<tr> <tr>
<td>Total users</td> <td>Total users</td>
<td>{{ctrl.stats.user_count}}</td> <td>{{ctrl.stats.users}}</td>
</tr>
<tr>
<td>Active users (seen last 14 days)</td>
<td>{{ctrl.stats.activeUsers}}</td>
</tr> </tr>
<tr> <tr>
<td>Total organizations</td> <td>Total organizations</td>
<td>{{ctrl.stats.org_count}}</td> <td>{{ctrl.stats.orgs}}</td>
</tr> </tr>
<tr> <tr>
<td>Total datasources</td> <td>Total datasources</td>
<td>{{ctrl.stats.data_source_count}}</td> <td>{{ctrl.stats.datasources}}</td>
</tr> </tr>
<tr> <tr>
<td>Total playlists</td> <td>Total playlists</td>
<td>{{ctrl.stats.playlist_count}}</td> <td>{{ctrl.stats.playlists}}</td>
</tr> </tr>
<tr> <tr>
<td>Total snapshots</td> <td>Total snapshots</td>
<td>{{ctrl.stats.db_snapshot_count}}</td> <td>{{ctrl.stats.snapshots}}</td>
</tr> </tr>
<tr> <tr>
<td>Total dashboard tags</td> <td>Total dashboard tags</td>
<td>{{ctrl.stats.db_tag_count}}</td> <td>{{ctrl.stats.tags}}</td>
</tr> </tr>
<tr> <tr>
<td>Total starred dashboards</td> <td>Total starred dashboards</td>
<td>{{ctrl.stats.starred_db_count}}</td> <td>{{ctrl.stats.stars}}</td>
</tr> </tr>
<tr> <tr>
<td>Total alerts</td> <td>Total alerts</td>
<td>{{ctrl.stats.alert_count}}</td> <td>{{ctrl.stats.alerts}}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
......
...@@ -25,7 +25,11 @@ ...@@ -25,7 +25,11 @@
<th>Name</th> <th>Name</th>
<th>Login</th> <th>Login</th>
<th>Email</th> <th>Email</th>
<th style="white-space: nowrap">Grafana Admin</th> <th>
Seen
<tip>Time since user was seen using Grafana</tip>
</th>
<th></th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
...@@ -35,7 +39,12 @@ ...@@ -35,7 +39,12 @@
<td>{{user.name}}</td> <td>{{user.name}}</td>
<td>{{user.login}}</td> <td>{{user.login}}</td>
<td>{{user.email}}</td> <td>{{user.email}}</td>
<td>{{user.isAdmin}}</td> <td>
{{user.lastSeenAtAge}}
</td>
<td>
<i class="fa fa-shield" ng-show="user.isAdmin" bs-tooltip="'Grafana Admin'"></i>
</td>
<td class="text-right"> <td class="text-right">
<a href="admin/users/edit/{{user.id}}" class="btn btn-inverse btn-small"> <a href="admin/users/edit/{{user.id}}" class="btn btn-inverse btn-small">
<i class="fa fa-edit"></i> <i class="fa fa-edit"></i>
......
...@@ -41,6 +41,10 @@ ...@@ -41,6 +41,10 @@
<tr> <tr>
<th>Login</th> <th>Login</th>
<th>Email</th> <th>Email</th>
<th>
Seen
<tip>Time since user was seen using Grafana</tip>
</th>
<th>Role</th> <th>Role</th>
<th style="width: 34px;"></th> <th style="width: 34px;"></th>
</tr> </tr>
...@@ -48,6 +52,7 @@ ...@@ -48,6 +52,7 @@
<tr ng-repeat="user in ctrl.users"> <tr ng-repeat="user in ctrl.users">
<td>{{user.login}}</td> <td>{{user.login}}</td>
<td><span class="ellipsis">{{user.email}}</span></td> <td><span class="ellipsis">{{user.email}}</span></td>
<td>{{user.lastSeenAtAge}}</td>
<td> <td>
<select type="text" ng-model="user.role" class="input-medium" ng-options="f for f in ['Viewer', 'Editor', 'Read Only Editor', 'Admin']" ng-change="ctrl.updateOrgUser(user)"> <select type="text" ng-model="user.role" class="input-medium" ng-options="f for f in ['Viewer', 'Editor', 'Read Only Editor', 'Admin']" ng-change="ctrl.updateOrgUser(user)">
</select> </select>
......
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