diff --git a/public/app/core/components/SharedPreferences/SharedPreferences.tsx b/public/app/core/components/SharedPreferences/SharedPreferences.tsx index b6d19f1f8af..82450a2320e 100644 --- a/public/app/core/components/SharedPreferences/SharedPreferences.tsx +++ b/public/app/core/components/SharedPreferences/SharedPreferences.tsx @@ -126,7 +126,7 @@ export class SharedPreferences extends PureComponent { getOptionLabel={i => i.title} onChange={(dashboard: DashboardSearchHit) => this.onHomeDashboardChanged(dashboard.id)} options={dashboards} - placeholder="Chose default dashboard" + placeholder="Choose default dashboard" width={20} /> diff --git a/public/app/features/admin/AdminEditUserCtrl.ts b/public/app/features/admin/AdminEditUserCtrl.ts index 12c8419b08b..27e2e445c94 100644 --- a/public/app/features/admin/AdminEditUserCtrl.ts +++ b/public/app/features/admin/AdminEditUserCtrl.ts @@ -1,12 +1,15 @@ import _ from 'lodash'; +import { dateTime } from '@grafana/ui'; import { BackendSrv } from 'app/core/services/backend_srv'; import { NavModelSrv } from 'app/core/core'; import { User } from 'app/core/services/context_srv'; +import { UserSession } from 'app/types'; export default class AdminEditUserCtrl { /** @ngInject */ constructor($scope: any, $routeParams: any, backendSrv: BackendSrv, $location: any, navModelSrv: NavModelSrv) { $scope.user = {}; + $scope.sessions = []; $scope.newOrg = { name: '', role: 'Editor' }; $scope.permissions = {}; $scope.navModel = navModelSrv.getNav('admin', 'global-users', 0); @@ -14,6 +17,7 @@ export default class AdminEditUserCtrl { $scope.init = () => { if ($routeParams.id) { $scope.getUser($routeParams.id); + $scope.getUserSessions($routeParams.id); $scope.getUserOrgs($routeParams.id); } }; @@ -26,6 +30,48 @@ export default class AdminEditUserCtrl { }); }; + $scope.getUserSessions = (id: number) => { + backendSrv.get('/api/admin/users/' + id + '/auth-tokens').then((sessions: UserSession[]) => { + sessions.reverse(); + + $scope.sessions = sessions.map((session: UserSession) => { + return { + id: session.id, + isActive: session.isActive, + seenAt: dateTime(session.seenAt).fromNow(), + createdAt: dateTime(session.createdAt).format('MMMM DD, YYYY'), + clientIp: session.clientIp, + browser: session.browser, + browserVersion: session.browserVersion, + os: session.os, + osVersion: session.osVersion, + device: session.device, + }; + }); + }); + }; + + $scope.revokeUserSession = (tokenId: number) => { + backendSrv + .post('/api/admin/users/' + $scope.user_id + '/revoke-auth-token', { + authTokenId: tokenId, + }) + .then(() => { + $scope.sessions = $scope.sessions.filter((session: UserSession) => { + if (session.id === tokenId) { + return false; + } + return true; + }); + }); + }; + + $scope.revokeAllUserSessions = (tokenId: number) => { + backendSrv.post('/api/admin/users/' + $scope.user_id + '/logout').then(() => { + $scope.sessions = []; + }); + }; + $scope.setPassword = () => { if (!$scope.passwordForm.$valid) { return; diff --git a/public/app/features/admin/partials/edit_user.html b/public/app/features/admin/partials/edit_user.html index 36591bf0200..a41eef89ad2 100644 --- a/public/app/features/admin/partials/edit_user.html +++ b/public/app/features/admin/partials/edit_user.html @@ -70,32 +70,66 @@ - - - - - - +
+
NameRole
+ + + + + + + + + + + +
NameRole
+ {{org.name}} Current + +
+ + + +
+
+ + + +
+ + +

Sessions

+ +
+ + + + + + + + + - + + + + + + + - - - -
Last seenLogged onIP addressBrowser & OS
Now{{session.seenAt}}{{session.createdAt}}{{session.clientIp}}{{session.browser}} on {{session.os}} {{session.osVersion}} - {{org.name}} Current + -
- - - -
-
- - - -
+ + + +
+ + diff --git a/public/app/features/alerting/state/reducers.ts b/public/app/features/alerting/state/reducers.ts index 96681923102..a47638752ac 100644 --- a/public/app/features/alerting/state/reducers.ts +++ b/public/app/features/alerting/state/reducers.ts @@ -13,9 +13,7 @@ function convertToAlertRule(dto: AlertRuleDTO, state: string): AlertRule { stateText: stateModel.text, stateIcon: stateModel.iconClass, stateClass: stateModel.stateClass, - stateAge: dateTime(dto.newStateDate) - .fromNow() - .replace(' ago', ''), + stateAge: dateTime(dto.newStateDate).fromNow(true), }; if (rule.state !== 'paused') { diff --git a/public/app/features/profile/ProfileCtrl.ts b/public/app/features/profile/ProfileCtrl.ts index b6d330a3216..a6a612bcaf5 100644 --- a/public/app/features/profile/ProfileCtrl.ts +++ b/public/app/features/profile/ProfileCtrl.ts @@ -1,11 +1,14 @@ import config from 'app/core/config'; import { coreModule } from 'app/core/core'; +import { dateTime } from '@grafana/ui'; +import { UserSession } from 'app/types'; export class ProfileCtrl { user: any; oldTheme: any; teams: any = []; orgs: any = []; + sessions: object[] = []; userForm: any; showTeamsList = false; showOrgsList = false; @@ -15,6 +18,7 @@ export class ProfileCtrl { /** @ngInject */ constructor(private backendSrv, private contextSrv, private $location, navModelSrv) { this.getUser(); + this.getUserSessions(); this.getUserTeams(); this.getUserOrgs(); this.navModel = navModelSrv.getNav('profile', 'profile-settings', 0); @@ -27,6 +31,52 @@ export class ProfileCtrl { }); } + getUserSessions() { + this.backendSrv.get('/api/user/auth-tokens').then((sessions: UserSession[]) => { + sessions.reverse(); + + const found = sessions.findIndex((session: UserSession) => { + return session.isActive; + }); + + if (found) { + const now = sessions[found]; + sessions.splice(found, found); + sessions.unshift(now); + } + + this.sessions = sessions.map((session: UserSession) => { + return { + id: session.id, + isActive: session.isActive, + seenAt: dateTime(session.seenAt).fromNow(), + createdAt: dateTime(session.createdAt).format('MMMM DD, YYYY'), + clientIp: session.clientIp, + browser: session.browser, + browserVersion: session.browserVersion, + os: session.os, + osVersion: session.osVersion, + device: session.device, + }; + }); + }); + } + + revokeUserSession(tokenId: number) { + this.backendSrv + .post('/api/user/revoke-auth-token', { + authTokenId: tokenId, + }) + .then(() => { + this.sessions = this.sessions.filter((session: UserSession) => { + if (session.id === tokenId) { + return false; + } + return true; + }); + }); + } + getUserTeams() { this.backendSrv.get('/api/user/teams').then(teams => { this.teams = teams; diff --git a/public/app/features/profile/partials/profile.html b/public/app/features/profile/partials/profile.html index 1cb800db6a5..461946dcf6e 100644 --- a/public/app/features/profile/partials/profile.html +++ b/public/app/features/profile/partials/profile.html @@ -74,3 +74,32 @@ + +

Sessions

+
+ + + + + + + + + + + + + + + + + + + + +
Last seenLogged onIP addressBrowser & OS
Now{{session.seenAt}}{{session.createdAt}}{{session.clientIp}}{{session.browser}} on {{session.os}} {{session.osVersion}} + +
+
diff --git a/public/app/types/user.ts b/public/app/types/user.ts index 535a6a3cc05..954bf278011 100644 --- a/public/app/types/user.ts +++ b/public/app/types/user.ts @@ -48,3 +48,16 @@ export interface UserState { orgId: number; timeZone: string; } + +export interface UserSession { + id: number; + createdAt: string; + clientIp: string; + isActive: boolean; + seenAt: string; + browser: string; + browserVersion: string; + os: string; + osVersion: string; + device: string; +}