mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
feat: store last seen date for users and present in stats and user lists, closes #9007
This commit is contained in:
parent
26ad025705
commit
e8a20643d6
@ -67,10 +67,10 @@ func updateTotalStats() {
|
||||
return
|
||||
}
|
||||
|
||||
M_StatTotal_Dashboards.Update(statsQuery.Result.DashboardCount)
|
||||
M_StatTotal_Users.Update(statsQuery.Result.UserCount)
|
||||
M_StatTotal_Playlists.Update(statsQuery.Result.PlaylistCount)
|
||||
M_StatTotal_Orgs.Update(statsQuery.Result.OrgCount)
|
||||
M_StatTotal_Dashboards.Update(statsQuery.Result.Dashboards)
|
||||
M_StatTotal_Users.Update(statsQuery.Result.Users)
|
||||
M_StatTotal_Playlists.Update(statsQuery.Result.Playlists)
|
||||
M_StatTotal_Orgs.Update(statsQuery.Result.Orgs)
|
||||
}
|
||||
}
|
||||
|
||||
@ -97,14 +97,16 @@ func sendUsageStats() {
|
||||
return
|
||||
}
|
||||
|
||||
metrics["stats.dashboards.count"] = statsQuery.Result.DashboardCount
|
||||
metrics["stats.users.count"] = statsQuery.Result.UserCount
|
||||
metrics["stats.orgs.count"] = statsQuery.Result.OrgCount
|
||||
metrics["stats.playlist.count"] = statsQuery.Result.PlaylistCount
|
||||
metrics["stats.dashboards.count"] = statsQuery.Result.Dashboards
|
||||
metrics["stats.users.count"] = statsQuery.Result.Users
|
||||
metrics["stats.orgs.count"] = statsQuery.Result.Orgs
|
||||
metrics["stats.playlist.count"] = statsQuery.Result.Playlists
|
||||
metrics["stats.plugins.apps.count"] = len(plugins.Apps)
|
||||
metrics["stats.plugins.panels.count"] = len(plugins.Panels)
|
||||
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{}
|
||||
if err := bus.Dispatch(&dsStats); err != nil {
|
||||
|
@ -62,6 +62,15 @@ func GetContextHandler() macaron.Handler {
|
||||
ctx.Data["ctx"] = 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 {
|
||||
|
||||
query := m.GetSignedInUserQuery{UserId: userId, OrgId: orgId}
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -103,9 +103,11 @@ type GetOrgUsersQuery struct {
|
||||
// Projections and DTOs
|
||||
|
||||
type OrgUserDTO struct {
|
||||
OrgId int64 `json:"orgId"`
|
||||
UserId int64 `json:"userId"`
|
||||
Email string `json:"email"`
|
||||
Login string `json:"login"`
|
||||
Role string `json:"role"`
|
||||
OrgId int64 `json:"orgId"`
|
||||
UserId int64 `json:"userId"`
|
||||
Email string `json:"email"`
|
||||
Login string `json:"login"`
|
||||
Role string `json:"role"`
|
||||
LastSeenAt time.Time `json:"lastSeenAt"`
|
||||
LastSeenAtAge string `json:"lastSeenAtAge"`
|
||||
}
|
||||
|
@ -1,11 +1,13 @@
|
||||
package models
|
||||
|
||||
type SystemStats struct {
|
||||
DashboardCount int64
|
||||
UserCount int64
|
||||
OrgCount int64
|
||||
PlaylistCount int64
|
||||
AlertCount int64
|
||||
Dashboards int64
|
||||
Datasources int64
|
||||
Users int64
|
||||
ActiveUsers int64
|
||||
Orgs int64
|
||||
Playlists int64
|
||||
Alerts int64
|
||||
}
|
||||
|
||||
type DataSourceStats struct {
|
||||
@ -22,15 +24,16 @@ type GetDataSourceStatsQuery struct {
|
||||
}
|
||||
|
||||
type AdminStats struct {
|
||||
UserCount int `json:"user_count"`
|
||||
OrgCount int `json:"org_count"`
|
||||
DashboardCount int `json:"dashboard_count"`
|
||||
DbSnapshotCount int `json:"db_snapshot_count"`
|
||||
DbTagCount int `json:"db_tag_count"`
|
||||
DataSourceCount int `json:"data_source_count"`
|
||||
PlaylistCount int `json:"playlist_count"`
|
||||
StarredDbCount int `json:"starred_db_count"`
|
||||
AlertCount int `json:"alert_count"`
|
||||
Users int `json:"users"`
|
||||
Orgs int `json:"orgs"`
|
||||
Dashboards int `json:"dashboards"`
|
||||
Snapshots int `json:"snapshots"`
|
||||
Tags int `json:"tags"`
|
||||
Datasources int `json:"datasources"`
|
||||
Playlists int `json:"playlists"`
|
||||
Stars int `json:"stars"`
|
||||
Alerts int `json:"alerts"`
|
||||
ActiveUsers int `json:"activeUsers"`
|
||||
}
|
||||
|
||||
type GetAdminStatsQuery struct {
|
||||
|
@ -33,8 +33,9 @@ type User struct {
|
||||
IsAdmin bool
|
||||
OrgId int64
|
||||
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
LastSeenAt time.Time
|
||||
}
|
||||
|
||||
func (u *User) NameOrFallback() string {
|
||||
@ -127,6 +128,7 @@ type GetUserProfileQuery struct {
|
||||
}
|
||||
|
||||
type SearchUsersQuery struct {
|
||||
OrgId int64
|
||||
Query string
|
||||
Page int
|
||||
Limit int
|
||||
@ -160,6 +162,15 @@ type SignedInUser struct {
|
||||
ApiKeyId int64
|
||||
IsGrafanaAdmin bool
|
||||
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 {
|
||||
@ -173,11 +184,13 @@ type UserProfileDTO struct {
|
||||
}
|
||||
|
||||
type UserSearchHitDTO struct {
|
||||
Id int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Login string `json:"login"`
|
||||
Email string `json:"email"`
|
||||
IsAdmin bool `json:"isAdmin"`
|
||||
Id int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Login string `json:"login"`
|
||||
Email string `json:"email"`
|
||||
IsAdmin bool `json:"isAdmin"`
|
||||
LastSeenAt time.Time `json:"lastSeenAt"`
|
||||
LastSeenAtAge string `json:"lastSeenAtAge"`
|
||||
}
|
||||
|
||||
type UserIdDTO struct {
|
||||
|
@ -103,4 +103,8 @@ func addUserMigrations(mg *Migrator) {
|
||||
{Name: "company", 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 (
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -71,11 +72,18 @@ func GetOrgUsers(query *m.GetOrgUsersQuery) error {
|
||||
sess := x.Table("org_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.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")
|
||||
|
||||
err := sess.Find(&query.Result)
|
||||
return err
|
||||
if err := sess.Find(&query.Result); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, user := range query.Result {
|
||||
user.LastSeenAtAge = util.GetAgeString(user.LastSeenAt)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func RemoveOrgUser(cmd *m.RemoveOrgUserCommand) error {
|
||||
|
@ -53,7 +53,7 @@ func EnsureAdminUser() {
|
||||
return
|
||||
}
|
||||
|
||||
if statsQuery.Result.UserCount > 0 {
|
||||
if statsQuery.Result.Users > 0 {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
@ -11,6 +13,8 @@ func init() {
|
||||
bus.AddHandler("sql", GetAdminStats)
|
||||
}
|
||||
|
||||
var activeUserTimeLimit time.Duration = time.Hour * 24 * 14
|
||||
|
||||
func GetDataSourceStats(query *m.GetDataSourceStatsQuery) error {
|
||||
var rawSql = `SELECT COUNT(*) as count, type FROM data_source GROUP BY type`
|
||||
query.Result = make([]*m.DataSourceStats, 0)
|
||||
@ -27,27 +31,35 @@ func GetSystemStats(query *m.GetSystemStatsQuery) error {
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM ` + dialect.Quote("user") + `
|
||||
) AS user_count,
|
||||
) AS users,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM ` + dialect.Quote("org") + `
|
||||
) AS org_count,
|
||||
) AS orgs,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM ` + dialect.Quote("dashboard") + `
|
||||
) AS dashboard_count,
|
||||
) AS dashboards,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM ` + dialect.Quote("data_source") + `
|
||||
) AS datasources,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM ` + dialect.Quote("playlist") + `
|
||||
) AS playlist_count,
|
||||
) AS playlists,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
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
|
||||
_, err := x.Sql(rawSql).Get(&stats)
|
||||
_, err := x.Sql(rawSql, activeUserDeadlineDate).Get(&stats)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -61,43 +73,48 @@ func GetAdminStats(query *m.GetAdminStatsQuery) error {
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM ` + dialect.Quote("user") + `
|
||||
) AS user_count,
|
||||
) AS users,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM ` + dialect.Quote("org") + `
|
||||
) AS org_count,
|
||||
) AS orgs,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM ` + dialect.Quote("dashboard") + `
|
||||
) AS dashboard_count,
|
||||
) AS dashboards,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM ` + dialect.Quote("dashboard_snapshot") + `
|
||||
) AS db_snapshot_count,
|
||||
) AS snapshots,
|
||||
(
|
||||
SELECT COUNT( DISTINCT ( ` + dialect.Quote("term") + ` ))
|
||||
FROM ` + dialect.Quote("dashboard_tag") + `
|
||||
) AS db_tag_count,
|
||||
) AS tags,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM ` + dialect.Quote("data_source") + `
|
||||
) AS data_source_count,
|
||||
) AS datasources,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM ` + dialect.Quote("playlist") + `
|
||||
) AS playlist_count,
|
||||
) AS playlists,
|
||||
(
|
||||
SELECT COUNT(DISTINCT ` + dialect.Quote("dashboard_id") + ` )
|
||||
FROM ` + dialect.Quote("star") + `
|
||||
) AS starred_db_count,
|
||||
SELECT COUNT(*) FROM ` + dialect.Quote("star") + `
|
||||
) AS stars,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
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
|
||||
_, err := x.Sql(rawSql).Get(&stats)
|
||||
_, err := x.Sql(rawSql, activeUserDeadlineDate).Get(&stats)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ func init() {
|
||||
bus.AddHandler("sql", GetUserByLogin)
|
||||
bus.AddHandler("sql", GetUserByEmail)
|
||||
bus.AddHandler("sql", SetUsingOrg)
|
||||
bus.AddHandler("sql", UpdateUserLastSeenAt)
|
||||
bus.AddHandler("sql", GetUserProfile)
|
||||
bus.AddHandler("sql", GetSignedInUser)
|
||||
bus.AddHandler("sql", SearchUsers)
|
||||
@ -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 {
|
||||
getOrgsForUserCmd := &m.GetUserOrgListQuery{UserId: cmd.UserId}
|
||||
GetUserOrgList(getOrgsForUserCmd)
|
||||
@ -324,15 +343,16 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error {
|
||||
}
|
||||
|
||||
var rawSql = `SELECT
|
||||
u.id as user_id,
|
||||
u.is_admin as is_grafana_admin,
|
||||
u.email as email,
|
||||
u.login as login,
|
||||
u.name as name,
|
||||
u.help_flags1 as help_flags1,
|
||||
org.name as org_name,
|
||||
org_user.role as org_role,
|
||||
org.id as org_id
|
||||
u.id as user_id,
|
||||
u.is_admin as is_grafana_admin,
|
||||
u.email as email,
|
||||
u.login as login,
|
||||
u.name as name,
|
||||
u.help_flags1 as help_flags1,
|
||||
u.last_seen_at as last_seen_at,
|
||||
org.name as org_name,
|
||||
org_user.role as org_role,
|
||||
org.id as org_id
|
||||
FROM ` + dialect.Quote("user") + ` as u
|
||||
LEFT OUTER JOIN org_user on org_user.org_id = ` + orgId + ` and org_user.user_id = u.id
|
||||
LEFT OUTER JOIN org on org.id = org_user.org_id `
|
||||
@ -367,27 +387,49 @@ func SearchUsers(query *m.SearchUsersQuery) error {
|
||||
query.Result = m.SearchUserQueryResult{
|
||||
Users: make([]*m.UserSearchHitDTO, 0),
|
||||
}
|
||||
|
||||
queryWithWildcards := "%" + query.Query + "%"
|
||||
|
||||
whereConditions := make([]string, 0)
|
||||
whereParams := make([]interface{}, 0)
|
||||
sess := x.Table("user")
|
||||
if query.Query != "" {
|
||||
sess.Where("email LIKE ? OR name LIKE ? OR login like ?", queryWithWildcards, queryWithWildcards, queryWithWildcards)
|
||||
|
||||
if query.OrgId > 0 {
|
||||
whereConditions = append(whereConditions, "org_id = ?")
|
||||
whereParams = append(whereParams, query.OrgId)
|
||||
}
|
||||
|
||||
if query.Query != "" {
|
||||
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)
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
// get total
|
||||
user := m.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)
|
||||
query.Result.TotalCount = count
|
||||
|
||||
for _, user := range query.Result.Users {
|
||||
user.LastSeenAtAge = util.GetAgeString(user.LastSeenAt)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,10 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"regexp"
|
||||
"time"
|
||||
)
|
||||
|
||||
func StringsFallback2(val1 string, val2 string) string {
|
||||
@ -28,3 +31,34 @@ func SplitString(str string) []string {
|
||||
|
||||
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
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
@ -24,3 +25,15 @@ func TestSplitString(t *testing.T) {
|
||||
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 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Total dashboards</td>
|
||||
<td>{{ctrl.stats.dashboard_count}}</td>
|
||||
<td>{{ctrl.stats.dashboards}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
<td>Total organizations</td>
|
||||
<td>{{ctrl.stats.org_count}}</td>
|
||||
<td>{{ctrl.stats.orgs}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total datasources</td>
|
||||
<td>{{ctrl.stats.data_source_count}}</td>
|
||||
<td>{{ctrl.stats.datasources}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total playlists</td>
|
||||
<td>{{ctrl.stats.playlist_count}}</td>
|
||||
<td>{{ctrl.stats.playlists}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total snapshots</td>
|
||||
<td>{{ctrl.stats.db_snapshot_count}}</td>
|
||||
<td>{{ctrl.stats.snapshots}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total dashboard tags</td>
|
||||
<td>{{ctrl.stats.db_tag_count}}</td>
|
||||
<td>{{ctrl.stats.tags}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total starred dashboards</td>
|
||||
<td>{{ctrl.stats.starred_db_count}}</td>
|
||||
<td>{{ctrl.stats.stars}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total alerts</td>
|
||||
<td>{{ctrl.stats.alert_count}}</td>
|
||||
<td>{{ctrl.stats.alerts}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -25,7 +25,11 @@
|
||||
<th>Name</th>
|
||||
<th>Login</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>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -35,7 +39,12 @@
|
||||
<td>{{user.name}}</td>
|
||||
<td>{{user.login}}</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">
|
||||
<a href="admin/users/edit/{{user.id}}" class="btn btn-inverse btn-small">
|
||||
<i class="fa fa-edit"></i>
|
||||
|
@ -41,6 +41,10 @@
|
||||
<tr>
|
||||
<th>Login</th>
|
||||
<th>Email</th>
|
||||
<th>
|
||||
Seen
|
||||
<tip>Time since user was seen using Grafana</tip>
|
||||
</th>
|
||||
<th>Role</th>
|
||||
<th style="width: 34px;"></th>
|
||||
</tr>
|
||||
@ -48,6 +52,7 @@
|
||||
<tr ng-repeat="user in ctrl.users">
|
||||
<td>{{user.login}}</td>
|
||||
<td><span class="ellipsis">{{user.email}}</span></td>
|
||||
<td>{{user.lastSeenAtAge}}</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>
|
||||
|
Loading…
Reference in New Issue
Block a user