diff --git a/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts
index 95aac4ef570..4ceed793cad 100644
--- a/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts
+++ b/packages/grafana-api-clients/src/clients/rtkq/legacy/endpoints.gen.ts
@@ -5340,6 +5340,7 @@ export type OrgUserDto = {
};
authLabels?: string[];
avatarUrl?: string;
+ created?: string;
email?: string;
isDisabled?: boolean;
isExternallySynced?: boolean;
@@ -6118,6 +6119,7 @@ export type ChangeUserPasswordCommand = {
export type UserSearchHitDto = {
authLabels?: string[];
avatarUrl?: string;
+ created?: string;
email?: string;
id?: number;
isAdmin?: boolean;
diff --git a/pkg/services/org/model.go b/pkg/services/org/model.go
index ac0268e051c..fa11375d778 100644
--- a/pkg/services/org/model.go
+++ b/pkg/services/org/model.go
@@ -151,7 +151,7 @@ type OrgUserDTO struct {
Role string `json:"role"`
LastSeenAt time.Time `json:"lastSeenAt"`
Updated time.Time `json:"-"`
- Created time.Time `json:"-"`
+ Created time.Time `json:"created"`
LastSeenAtAge string `json:"lastSeenAtAge"`
AccessControl map[string]bool `json:"accessControl,omitempty"`
IsDisabled bool `json:"isDisabled"`
diff --git a/pkg/services/user/model.go b/pkg/services/user/model.go
index fc8d9589357..12b0ac6ed57 100644
--- a/pkg/services/user/model.go
+++ b/pkg/services/user/model.go
@@ -146,6 +146,7 @@ type UserSearchHitDTO struct {
LastSeenAtAge string `json:"lastSeenAtAge"`
AuthLabels []string `json:"authLabels"`
AuthModule AuthModuleConversion `json:"-"`
+ Created time.Time `json:"created" xorm:"created"`
}
type GetUserProfileQuery struct {
diff --git a/pkg/services/user/userimpl/store.go b/pkg/services/user/userimpl/store.go
index a7773147373..64780c5c2eb 100644
--- a/pkg/services/user/userimpl/store.go
+++ b/pkg/services/user/userimpl/store.go
@@ -541,7 +541,7 @@ func (ss *sqlStore) Search(ctx context.Context, query *user.SearchUsersQuery) (*
sess.Limit(query.Limit, offset)
}
- sess.Cols("u.id", "u.uid", "u.email", "u.name", "u.login", "u.is_admin", "u.is_disabled", "u.last_seen_at", "user_auth.auth_module", "u.is_provisioned")
+ sess.Cols("u.id", "u.uid", "u.email", "u.name", "u.login", "u.is_admin", "u.is_disabled", "u.last_seen_at", "user_auth.auth_module", "u.is_provisioned", "u.created")
if len(query.SortOpts) > 0 {
for i := range query.SortOpts {
diff --git a/public/api-enterprise-spec.json b/public/api-enterprise-spec.json
index b381b7e38fd..4cb15addb0a 100644
--- a/public/api-enterprise-spec.json
+++ b/public/api-enterprise-spec.json
@@ -6002,6 +6002,10 @@
"avatarUrl": {
"type": "string"
},
+ "created": {
+ "type": "string",
+ "format": "date-time"
+ },
"email": {
"type": "string"
},
@@ -8903,6 +8907,10 @@
"avatarUrl": {
"type": "string"
},
+ "created": {
+ "type": "string",
+ "format": "date-time"
+ },
"email": {
"type": "string"
},
diff --git a/public/api-merged.json b/public/api-merged.json
index 057251b12bc..d511139c7bd 100644
--- a/public/api-merged.json
+++ b/public/api-merged.json
@@ -18489,6 +18489,10 @@
"avatarUrl": {
"type": "string"
},
+ "created": {
+ "type": "string",
+ "format": "date-time"
+ },
"email": {
"type": "string"
},
@@ -23410,6 +23414,10 @@
"avatarUrl": {
"type": "string"
},
+ "created": {
+ "type": "string",
+ "format": "date-time"
+ },
"email": {
"type": "string"
},
diff --git a/public/app/features/admin/Users/OrgUsersTable.tsx b/public/app/features/admin/Users/OrgUsersTable.tsx
index 3debbd886a8..a3d93b12bad 100644
--- a/public/app/features/admin/Users/OrgUsersTable.tsx
+++ b/public/app/features/admin/Users/OrgUsersTable.tsx
@@ -115,12 +115,15 @@ export const OrgUsersTable = ({
{
id: 'lastSeenAtAge',
header: 'Last active',
- cell: ({ cell: { value } }: Cell<'lastSeenAtAge'>) => {
+ cell: ({ cell: { value }, row: { original } }: Cell<'lastSeenAtAge'>) => {
+ // If lastSeenAt is before created, user has never logged in
+ const neverLoggedIn =
+ original.lastSeenAt && original.created && new Date(original.lastSeenAt) < new Date(original.created);
return (
<>
{value && (
<>
- {value === '10 years' ? (
+ {neverLoggedIn ? (
Never
diff --git a/public/app/features/admin/Users/UsersTable.tsx b/public/app/features/admin/Users/UsersTable.tsx
index a3a1f6be297..b4d34f0d438 100644
--- a/public/app/features/admin/Users/UsersTable.tsx
+++ b/public/app/features/admin/Users/UsersTable.tsx
@@ -135,12 +135,19 @@ export const UsersTable = ({
content: 'Time since user was seen using Grafana',
iconName: 'question-circle',
},
- cell: ({ cell: { value } }: Cell<'lastSeenAtAge'>) => {
+ cell: ({
+ cell: { value },
+ row: {
+ original: { lastSeenAt, created },
+ },
+ }: Cell<'lastSeenAtAge'>) => {
+ // The user has never logged in if lastSeenAt is before its creation date.
+ const neverLoggedIn = lastSeenAt && created && new Date(lastSeenAt) < new Date(created);
return (
<>
{value && (
<>
- {value === '10 years' ? (
+ {neverLoggedIn ? (
Never
diff --git a/public/app/types/user.ts b/public/app/types/user.ts
index e5cd1805e7f..9e575e2ad93 100644
--- a/public/app/types/user.ts
+++ b/public/app/types/user.ts
@@ -5,6 +5,7 @@ import { Role } from './accessControl';
export interface OrgUser extends WithAccessControlMetadata {
avatarUrl: string;
email: string;
+ created?: string;
lastSeenAt: string;
lastSeenAtAge: string;
login: string;
@@ -49,6 +50,7 @@ export interface UserDTO extends WithAccessControlMetadata {
theme?: string;
avatarUrl?: string;
orgId?: number;
+ created?: string;
lastSeenAt?: string;
lastSeenAtAge?: string;
licensedRole?: string;
diff --git a/public/openapi3.json b/public/openapi3.json
index 589a49e68ba..4a7c5f6be08 100644
--- a/public/openapi3.json
+++ b/public/openapi3.json
@@ -8018,6 +8018,10 @@
"avatarUrl": {
"type": "string"
},
+ "created": {
+ "format": "date-time",
+ "type": "string"
+ },
"email": {
"type": "string"
},
@@ -12938,6 +12942,10 @@
"avatarUrl": {
"type": "string"
},
+ "created": {
+ "format": "date-time",
+ "type": "string"
+ },
"email": {
"type": "string"
},