Chore: Mark up User Profile page for translation (#43874)

* Mark up User profile page for translation

* Extract new messages

* updated selectors

* update selectors

* wip TestProvider

* update tests

* fix field labels

* extract new messages

* don't store date objects in redux state

* don't store date objects in redux state
This commit is contained in:
Josh Hunt 2022-01-17 16:58:49 +00:00 committed by GitHub
parent 6d072ad84d
commit 36983d8d3b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 629 additions and 66 deletions

View File

@ -2,7 +2,8 @@
"locales": [ "locales": [
"en", "en",
"fr", "fr",
"es" "es",
"pseudo-LOCALE"
], ],
"catalogs": [ "catalogs": [
{ {
@ -11,14 +12,18 @@
"public/app" "public/app"
], ],
"exclude": [ "exclude": [
"**/*.d.ts",
"**/*.test.ts",
"**/node_modules/**", "**/node_modules/**",
"public/app/plugins" "public/app/plugins"
] ]
} }
], ],
"fallbackLocales": { "fallbackLocales": {
"pseudo-LOCALE": "en",
"default": "en" "default": "en"
}, },
"pseudoLocale": "pseudo-LOCALE",
"sourceLocale": "en", "sourceLocale": "en",
"format": "po", "format": "po",
"formatOptions": { "formatOptions": {

View File

@ -322,4 +322,10 @@ export const Components = {
DashboardRow: { DashboardRow: {
title: (title: string) => `data-testid dashboard-row-title-${title}`, title: (title: string) => `data-testid dashboard-row-title-${title}`,
}, },
UserProfile: {
profileSaveButton: 'data-testid-user-profile-save',
preferencesSaveButton: 'data-testid-shared-prefs-save',
orgsTable: 'data-testid-user-orgs-table',
sessionsTable: 'data-testid-user-sessions-table',
},
}; };

View File

@ -21,6 +21,7 @@ import { selectors } from '@grafana/e2e-selectors';
import { DashboardSearchHit, DashboardSearchItemType } from 'app/features/search/types'; import { DashboardSearchHit, DashboardSearchItemType } from 'app/features/search/types';
import { backendSrv } from 'app/core/services/backend_srv'; import { backendSrv } from 'app/core/services/backend_srv';
import { PreferencesService } from 'app/core/services/PreferencesService'; import { PreferencesService } from 'app/core/services/PreferencesService';
import { t, Trans } from '@lingui/macro';
export interface Props { export interface Props {
resourceUri: string; resourceUri: string;
@ -36,9 +37,9 @@ export interface State {
} }
const themes: SelectableValue[] = [ const themes: SelectableValue[] = [
{ value: '', label: 'Default' }, { value: '', label: t({ id: 'shared-preferences.theme.default-label', message: 'Default' }) },
{ value: 'dark', label: 'Dark' }, { value: 'dark', label: t({ id: 'shared-preferences.theme.dark-label', message: 'Dark' }) },
{ value: 'light', label: 'Light' }, { value: 'light', label: t({ id: 'shared-preferences.theme.light-label', message: 'Light' }) },
]; ];
export class SharedPreferences extends PureComponent<Props, State> { export class SharedPreferences extends PureComponent<Props, State> {
@ -130,12 +131,24 @@ export class SharedPreferences extends PureComponent<Props, State> {
const { disabled } = this.props; const { disabled } = this.props;
const styles = getStyles(); const styles = getStyles();
const homeDashboardTooltip = (
<Tooltip
content={
<Trans id="shared-preferences.fields.home-dashboard-tooltip">
Not finding the dashboard you want? Star it first, then it should appear in this select box.
</Trans>
}
>
<Icon name="info-circle" />
</Tooltip>
);
return ( return (
<Form onSubmit={this.onSubmitForm}> <Form onSubmit={this.onSubmitForm}>
{() => { {() => {
return ( return (
<FieldSet label="Preferences" disabled={disabled}> <FieldSet label={<Trans id="shared-preferences.title">Preferences</Trans>} disabled={disabled}>
<Field label="UI Theme"> <Field label={t({ id: 'shared-preferences.fields.theme-label', message: 'UI Theme' })}>
<RadioButtonGroup <RadioButtonGroup
options={themes} options={themes}
value={themes.find((item) => item.value === theme)?.value} value={themes.find((item) => item.value === theme)?.value}
@ -146,10 +159,11 @@ export class SharedPreferences extends PureComponent<Props, State> {
<Field <Field
label={ label={
<Label htmlFor="home-dashboard-select"> <Label htmlFor="home-dashboard-select">
<span className={styles.labelText}>Home Dashboard</span> <span className={styles.labelText}>
<Tooltip content="Not finding the dashboard you want? Star it first, then it should appear in this select box."> <Trans id="shared-preferences.fields.home-dashboard-label">Home Dashboard</Trans>
<Icon name="info-circle" /> </span>
</Tooltip>
{homeDashboardTooltip}
</Label> </Label>
} }
data-testid="User preferences home dashboard drop down" data-testid="User preferences home dashboard drop down"
@ -163,30 +177,40 @@ export class SharedPreferences extends PureComponent<Props, State> {
this.onHomeDashboardChanged(dashboard.id) this.onHomeDashboardChanged(dashboard.id)
} }
options={dashboards} options={dashboards}
placeholder="Choose default dashboard" placeholder={t({
id: 'shared-preferences.fields.home-dashboard-placeholder',
message: 'Choose default dashboard',
})}
inputId="home-dashboard-select" inputId="home-dashboard-select"
/> />
</Field> </Field>
<Field label="Timezone" data-testid={selectors.components.TimeZonePicker.containerV2}> <Field
label={t({ id: 'shared-dashboard.fields.timezone-label', message: 'Timezone' })}
data-testid={selectors.components.TimeZonePicker.containerV2}
>
<TimeZonePicker <TimeZonePicker
includeInternal={true} includeInternal={true}
value={timezone} value={timezone}
onChange={this.onTimeZoneChanged} onChange={this.onTimeZoneChanged}
inputId={'shared-preferences-timezone-picker'} inputId="shared-preferences-timezone-picker"
/> />
</Field> </Field>
<Field label="Week start" data-testid={selectors.components.WeekStartPicker.containerV2}> <Field
label={t({ id: 'shared-preferences.fields.week-start-label', message: 'Week start' })}
data-testid={selectors.components.WeekStartPicker.containerV2}
>
<WeekStartPicker <WeekStartPicker
value={weekStart} value={weekStart}
onChange={this.onWeekStartChanged} onChange={this.onWeekStartChanged}
inputId={'shared-preferences-week-start-picker'} inputId={'shared-preferences-week-start-picker'}
/> />
</Field> </Field>
<div className="gf-form-button-row"> <div className="gf-form-button-row">
<Button variant="primary" aria-label="User preferences save button"> <Button variant="primary" data-testid={selectors.components.UserProfile.preferencesSaveButton}>
Save <Trans id="common.save">Save</Trans>
</Button> </Button>
</div> </div>
</FieldSet> </FieldSet>

View File

@ -3,8 +3,9 @@ import { css } from '@emotion/css';
import { ConfirmButton, ConfirmModal, Button } from '@grafana/ui'; import { ConfirmButton, ConfirmModal, Button } from '@grafana/ui';
import { AccessControlAction, UserSession } from 'app/types'; import { AccessControlAction, UserSession } from 'app/types';
import { contextSrv } from 'app/core/core'; import { contextSrv } from 'app/core/core';
import { withI18n, withI18nProps } from '@lingui/react';
interface Props { interface Props extends withI18nProps {
sessions: UserSession[]; sessions: UserSession[];
onSessionRevoke: (id: number) => void; onSessionRevoke: (id: number) => void;
@ -15,7 +16,7 @@ interface State {
showLogoutModal: boolean; showLogoutModal: boolean;
} }
export class UserSessions extends PureComponent<Props, State> { class BaseUserSessions extends PureComponent<Props, State> {
forceAllLogoutButton = React.createRef<HTMLButtonElement>(); forceAllLogoutButton = React.createRef<HTMLButtonElement>();
state: State = { state: State = {
showLogoutModal: false, showLogoutModal: false,
@ -43,7 +44,7 @@ export class UserSessions extends PureComponent<Props, State> {
}; };
render() { render() {
const { sessions } = this.props; const { sessions, i18n } = this.props;
const { showLogoutModal } = this.state; const { showLogoutModal } = this.state;
const logoutFromAllDevicesClass = css` const logoutFromAllDevicesClass = css`
@ -71,7 +72,7 @@ export class UserSessions extends PureComponent<Props, State> {
sessions.map((session, index) => ( sessions.map((session, index) => (
<tr key={`${session.id}-${index}`}> <tr key={`${session.id}-${index}`}>
<td>{session.isActive ? 'Now' : session.seenAt}</td> <td>{session.isActive ? 'Now' : session.seenAt}</td>
<td>{session.createdAt}</td> <td>{i18n.date(session.createdAt, { dateStyle: 'long' })}</td>
<td>{session.clientIp}</td> <td>{session.clientIp}</td>
<td>{`${session.browser} on ${session.os} ${session.osVersion}`}</td> <td>{`${session.browser} on ${session.os} ${session.osVersion}`}</td>
<td> <td>
@ -113,3 +114,5 @@ export class UserSessions extends PureComponent<Props, State> {
); );
} }
} }
export const UserSessions = withI18n()(BaseUserSessions);

View File

@ -1,5 +1,5 @@
import config from 'app/core/config'; import config from 'app/core/config';
import { dateTimeFormat, dateTimeFormatTimeAgo } from '@grafana/data'; import { dateTimeFormatTimeAgo } from '@grafana/data';
import { featureEnabled, getBackendSrv, locationService } from '@grafana/runtime'; import { featureEnabled, getBackendSrv, locationService } from '@grafana/runtime';
import { ThunkResult, LdapUser, UserSession, UserDTO, AccessControlAction, UserFilter } from 'app/types'; import { ThunkResult, LdapUser, UserSession, UserDTO, AccessControlAction, UserFilter } from 'app/types';
@ -144,12 +144,13 @@ export function loadUserSessions(userId: number): ThunkResult<void> {
const tokens = await getBackendSrv().get(`/api/admin/users/${userId}/auth-tokens`); const tokens = await getBackendSrv().get(`/api/admin/users/${userId}/auth-tokens`);
tokens.reverse(); tokens.reverse();
const sessions = tokens.map((session: UserSession) => { const sessions = tokens.map((session: UserSession) => {
return { return {
id: session.id, id: session.id,
isActive: session.isActive, isActive: session.isActive,
seenAt: dateTimeFormatTimeAgo(session.seenAt), seenAt: dateTimeFormatTimeAgo(session.seenAt),
createdAt: dateTimeFormat(session.createdAt, { format: 'MMMM DD, YYYY' }), createdAt: session.createdAt,
clientIp: session.clientIp, clientIp: session.clientIp,
browser: session.browser, browser: session.browser,
browserVersion: session.browserVersion, browserVersion: session.browserVersion,
@ -158,6 +159,7 @@ export function loadUserSessions(userId: number): ThunkResult<void> {
device: session.device, device: session.device,
}; };
}); });
dispatch(userSessionsLoadedAction(sessions)); dispatch(userSessionsLoadedAction(sessions));
}; };
} }

View File

@ -1,6 +1,8 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { UserDTO, UserOrg } from 'app/types'; import { UserDTO, UserOrg } from 'app/types';
import { Button, LoadingPlaceholder } from '@grafana/ui'; import { Button, LoadingPlaceholder } from '@grafana/ui';
import { Trans } from '@lingui/macro';
import { selectors } from '@grafana/e2e-selectors';
export interface Props { export interface Props {
user: UserDTO | null; user: UserDTO | null;
@ -23,13 +25,20 @@ export class UserOrganizations extends PureComponent<Props> {
return ( return (
<div> <div>
<h3 className="page-sub-heading">Organizations</h3> <h3 className="page-sub-heading">
<Trans id="user-orgs.title">Organizations</Trans>
</h3>
<div className="gf-form-group"> <div className="gf-form-group">
<table className="filter-table form-inline" aria-label="User organizations table"> <table className="filter-table form-inline" data-testid={selectors.components.UserProfile.orgsTable}>
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>
<th>Role</th> <Trans id="user-orgs.name-column">Name</Trans>
</th>
<th>
<Trans id="user-orgs.role-column">Role</Trans>
</th>
<th /> <th />
</tr> </tr>
</thead> </thead>
@ -42,7 +51,7 @@ export class UserOrganizations extends PureComponent<Props> {
<td className="text-right"> <td className="text-right">
{org.orgId === user?.orgId ? ( {org.orgId === user?.orgId ? (
<Button variant="secondary" size="sm" disabled> <Button variant="secondary" size="sm" disabled>
Current <Trans id="user-orgs.current-org-button">Current</Trans>
</Button> </Button>
) : ( ) : (
<Button <Button
@ -51,9 +60,8 @@ export class UserOrganizations extends PureComponent<Props> {
onClick={() => { onClick={() => {
this.props.setUserOrg(org); this.props.setUserOrg(org);
}} }}
aria-label={`Switch to the organization named ${org.name}`}
> >
Select <Trans id="user-orgs.select-org-button">Select organisation</Trans>
</Button> </Button>
)} )}
</td> </td>

View File

@ -1,6 +1,7 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { Trans } from '@lingui/macro'; import { Trans, t } from '@lingui/macro';
import { Button, Field, FieldSet, Form, Icon, Input, Tooltip } from '@grafana/ui'; import { Button, Field, FieldSet, Form, Icon, Input, Tooltip } from '@grafana/ui';
import { selectors } from '@grafana/e2e-selectors';
import { UserDTO } from 'app/types'; import { UserDTO } from 'app/types';
import config from 'app/core/config'; import config from 'app/core/config';
import { ProfileUpdateFields } from './types'; import { ProfileUpdateFields } from './types';
@ -22,37 +23,57 @@ export const UserProfileEditForm: FC<Props> = ({ user, isSavingUser, updateProfi
<Form onSubmit={onSubmitProfileUpdate} validateOn="onBlur"> <Form onSubmit={onSubmitProfileUpdate} validateOn="onBlur">
{({ register, errors }) => { {({ register, errors }) => {
return ( return (
<FieldSet label={<Trans id="edit-user-profile.title">Edit profile</Trans>}> <FieldSet label={<Trans id="user-profile.title">Edit profile</Trans>}>
<Field label="Name" invalid={!!errors.name} error="Name is required" disabled={disableLoginForm}> <Field
label={t({ id: 'user-profile.fields.name-label', message: 'Name' })}
invalid={!!errors.name}
error={<Trans id="user-profile.fields.name-error">Name is required</Trans>}
disabled={disableLoginForm}
>
<Input <Input
{...register('name', { required: true })} {...register('name', { required: true })}
id="edit-user-profile-name" id="edit-user-profile-name"
placeholder="Name" placeholder={t({ id: 'user-profile.fields.name-label', message: 'Name' })}
defaultValue={user?.name ?? ''} defaultValue={user?.name ?? ''}
suffix={<InputSuffix />} suffix={<InputSuffix />}
/> />
</Field> </Field>
<Field label="Email" invalid={!!errors.email} error="Email is required" disabled={disableLoginForm}>
<Field
label={t({ id: 'user-profile.fields.email-label', message: 'Email' })}
invalid={!!errors.email}
error={<Trans id="user-profile.fields.email-error">Email is required</Trans>}
disabled={disableLoginForm}
>
<Input <Input
{...register('email', { required: true })} {...register('email', { required: true })}
id="edit-user-profile-email" id="edit-user-profile-email"
placeholder="Email" placeholder={t({ id: 'user-profile.fields.email-label', message: 'Email' })}
defaultValue={user?.email ?? ''} defaultValue={user?.email ?? ''}
suffix={<InputSuffix />} suffix={<InputSuffix />}
/> />
</Field> </Field>
<Field label="Username" disabled={disableLoginForm}>
<Field
label={t({ id: 'user-profile.fields.username-label', message: 'Username' })}
disabled={disableLoginForm}
>
<Input <Input
{...register('login')} {...register('login')}
id="edit-user-profile-username" id="edit-user-profile-username"
defaultValue={user?.login ?? ''} defaultValue={user?.login ?? ''}
placeholder="Username" placeholder={t({ id: 'user-profile.fields.username-label', message: 'Username' })}
suffix={<InputSuffix />} suffix={<InputSuffix />}
/> />
</Field> </Field>
<div className="gf-form-button-row"> <div className="gf-form-button-row">
<Button variant="primary" disabled={isSavingUser} aria-label="Edit user profile save button"> <Button
Save variant="primary"
disabled={isSavingUser}
data-testid={selectors.components.UserProfile.profileSaveButton}
>
<Trans id="common.save">Save</Trans>
</Button> </Button>
</div> </div>
</FieldSet> </FieldSet>

View File

@ -4,6 +4,7 @@ import userEvent from '@testing-library/user-event';
import { within } from '@testing-library/dom'; import { within } from '@testing-library/dom';
import { OrgRole } from '@grafana/data'; import { OrgRole } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import TestProvider from '../../../test/helpers/TestProvider';
import { Props, UserProfileEditPage } from './UserProfileEditPage'; import { Props, UserProfileEditPage } from './UserProfileEditPage';
import { initialUserState } from './state/reducers'; import { initialUserState } from './state/reducers';
@ -90,28 +91,28 @@ function getSelectors() {
const dashboardSelect = () => screen.getByTestId('User preferences home dashboard drop down'); const dashboardSelect = () => screen.getByTestId('User preferences home dashboard drop down');
const timepickerSelect = () => screen.getByTestId(selectors.components.TimeZonePicker.containerV2); const timepickerSelect = () => screen.getByTestId(selectors.components.TimeZonePicker.containerV2);
const teamsTable = () => screen.getByRole('table', { name: /user teams table/i }); const teamsTable = () => screen.getByRole('table', { name: /user teams table/i });
const orgsTable = () => screen.getByRole('table', { name: /user organizations table/i }); const orgsTable = () => screen.getByTestId(selectors.components.UserProfile.orgsTable);
const sessionsTable = () => screen.getByRole('table', { name: /user sessions table/i }); const sessionsTable = () => screen.getByTestId(selectors.components.UserProfile.sessionsTable);
return { return {
name: () => screen.getByRole('textbox', { name: /^name$/i }), name: () => screen.getByRole('textbox', { name: /^name$/i }),
email: () => screen.getByRole('textbox', { name: /email/i }), email: () => screen.getByRole('textbox', { name: /email/i }),
username: () => screen.getByRole('textbox', { name: /username/i }), username: () => screen.getByRole('textbox', { name: /username/i }),
saveProfile: () => screen.getByRole('button', { name: /edit user profile save button/i }), saveProfile: () => screen.getByTestId(selectors.components.UserProfile.profileSaveButton),
dashboardSelect, dashboardSelect,
dashboardValue: () => within(dashboardSelect()).getByText(/default/i), dashboardValue: () => within(dashboardSelect()).getByText(/default/i),
timepickerSelect, timepickerSelect,
timepickerValue: () => within(timepickerSelect()).getByText(/coordinated universal time/i), timepickerValue: () => within(timepickerSelect()).getByText(/coordinated universal time/i),
savePreferences: () => screen.getByRole('button', { name: /user preferences save button/i }), savePreferences: () => screen.getByTestId(selectors.components.UserProfile.preferencesSaveButton),
teamsTable, teamsTable,
teamsRow: () => within(teamsTable()).getByRole('row', { name: /team one team.one@test\.com 2000/i }), teamsRow: () => within(teamsTable()).getByRole('row', { name: /team one team.one@test\.com 2000/i }),
orgsTable, orgsTable,
orgsEditorRow: () => within(orgsTable()).getByRole('row', { name: /main editor current/i }), orgsEditorRow: () => within(orgsTable()).getByRole('row', { name: /main editor current/i }),
orgsViewerRow: () => within(orgsTable()).getByRole('row', { name: /second viewer select/i }), orgsViewerRow: () => within(orgsTable()).getByRole('row', { name: /second viewer select organisation/i }),
orgsAdminRow: () => within(orgsTable()).getByRole('row', { name: /third admin select/i }), orgsAdminRow: () => within(orgsTable()).getByRole('row', { name: /third admin select organisation/i }),
sessionsTable, sessionsTable,
sessionsRow: () => sessionsRow: () =>
within(sessionsTable()).getByRole('row', { within(sessionsTable()).getByRole('row', {
name: /now 2021-01-01 04:00:00 localhost chrome on mac os x 11/i, name: /now January 1, 2021 localhost chrome on mac os x 11/i,
}), }),
}; };
} }
@ -125,7 +126,11 @@ async function getTestContext(overrides: Partial<Props> = {}) {
const searchSpy = jest.spyOn(backendSrv, 'search').mockResolvedValue([]); const searchSpy = jest.spyOn(backendSrv, 'search').mockResolvedValue([]);
const props = { ...defaultProps, ...overrides }; const props = { ...defaultProps, ...overrides };
const { rerender } = render(<UserProfileEditPage {...props} />); const { rerender } = render(
<TestProvider>
<UserProfileEditPage {...props} />
</TestProvider>
);
await waitFor(() => expect(props.initUserProfilePage).toHaveBeenCalledTimes(1)); await waitFor(() => expect(props.initUserProfilePage).toHaveBeenCalledTimes(1));
@ -253,7 +258,7 @@ describe('UserProfileEditPage', () => {
const { props } = await getTestContext(); const { props } = await getTestContext();
const orgsAdminSelectButton = () => const orgsAdminSelectButton = () =>
within(getSelectors().orgsAdminRow()).getByRole('button', { within(getSelectors().orgsAdminRow()).getByRole('button', {
name: /switch to the organization named Third/i, name: /select organisation/i,
}); });
userEvent.click(orgsAdminSelectButton()); userEvent.click(orgsAdminSelectButton());

View File

@ -1,19 +1,22 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { UserSession } from 'app/types'; import { UserSession } from 'app/types';
import { Button, Icon, LoadingPlaceholder } from '@grafana/ui'; import { Button, Icon, LoadingPlaceholder } from '@grafana/ui';
import { withI18n, withI18nProps } from '@lingui/react';
import { t, Trans } from '@lingui/macro';
import { selectors } from '@grafana/e2e-selectors';
export interface Props { interface Props extends withI18nProps {
sessions: UserSession[]; sessions: UserSession[];
isLoading: boolean; isLoading: boolean;
revokeUserSession: (tokenId: number) => void; revokeUserSession: (tokenId: number) => void;
} }
export class UserSessions extends PureComponent<Props> { class UserSessions extends PureComponent<Props> {
render() { render() {
const { isLoading, sessions, revokeUserSession } = this.props; const { isLoading, sessions, revokeUserSession, i18n } = this.props;
if (isLoading) { if (isLoading) {
return <LoadingPlaceholder text="Loading sessions..." />; return <LoadingPlaceholder text={<Trans id="user-sessions.loading">Loading sessions...</Trans>} />;
} }
return ( return (
@ -22,13 +25,21 @@ export class UserSessions extends PureComponent<Props> {
<> <>
<h3 className="page-sub-heading">Sessions</h3> <h3 className="page-sub-heading">Sessions</h3>
<div className="gf-form-group"> <div className="gf-form-group">
<table className="filter-table form-inline" aria-label="User sessions table"> <table className="filter-table form-inline" data-testid={selectors.components.UserProfile.sessionsTable}>
<thead> <thead>
<tr> <tr>
<th>Last seen</th> <th>
<th>Logged on</th> <Trans id="user-session.seen-at-column">Last seen</Trans>
<th>IP address</th> </th>
<th>Browser &amp; OS</th> <th>
<Trans id="user-session.created-at-column">Logged on</Trans>
</th>
<th>
<Trans id="user-session.ip-column">IP address</Trans>
</th>
<th>
<Trans id="user-session.browser-column">Browser &amp; OS</Trans>
</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -36,7 +47,7 @@ export class UserSessions extends PureComponent<Props> {
{sessions.map((session: UserSession, index) => ( {sessions.map((session: UserSession, index) => (
<tr key={index}> <tr key={index}>
{session.isActive ? <td>Now</td> : <td>{session.seenAt}</td>} {session.isActive ? <td>Now</td> : <td>{session.seenAt}</td>}
<td>{session.createdAt}</td> <td>{i18n.date(session.createdAt, { dateStyle: 'long' })}</td>
<td>{session.clientIp}</td> <td>{session.clientIp}</td>
<td> <td>
{session.browser} on {session.os} {session.osVersion} {session.browser} on {session.os} {session.osVersion}
@ -46,7 +57,7 @@ export class UserSessions extends PureComponent<Props> {
size="sm" size="sm"
variant="destructive" variant="destructive"
onClick={() => revokeUserSession(session.id)} onClick={() => revokeUserSession(session.id)}
aria-label="Revoke user session" aria-label={t({ id: 'user-session.revoke', message: 'Revoke user session' })}
> >
<Icon name="power" /> <Icon name="power" />
</Button> </Button>
@ -63,4 +74,4 @@ export class UserSessions extends PureComponent<Props> {
} }
} }
export default UserSessions; export default withI18n()(UserSessions);

View File

@ -166,7 +166,7 @@ describe('userReducer', () => {
browserVersion: '90', browserVersion: '90',
osVersion: '95', osVersion: '95',
clientIp: '192.168.1.1', clientIp: '192.168.1.1',
createdAt: 'December 31, 2020', createdAt: '2021-01-01 04:00:00',
device: 'Computer', device: 'Computer',
os: 'Windows', os: 'Windows',
isActive: false, isActive: false,

View File

@ -1,6 +1,6 @@
import { isEmpty, isString, set } from 'lodash'; import { isEmpty, isString, set } from 'lodash';
import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { dateTimeFormat, dateTimeFormatTimeAgo, setWeekStart, TimeZone } from '@grafana/data'; import { dateTimeFormatTimeAgo, setWeekStart, TimeZone } from '@grafana/data';
import { Team, ThunkResult, UserDTO, UserOrg, UserSession } from 'app/types'; import { Team, ThunkResult, UserDTO, UserOrg, UserSession } from 'app/types';
import config from 'app/core/config'; import config from 'app/core/config';
@ -78,7 +78,7 @@ export const slice = createSlice({
id: session.id, id: session.id,
isActive: session.isActive, isActive: session.isActive,
seenAt: dateTimeFormatTimeAgo(session.seenAt), seenAt: dateTimeFormatTimeAgo(session.seenAt),
createdAt: dateTimeFormat(session.createdAt, { format: 'MMMM DD, YYYY' }), createdAt: session.createdAt,
clientIp: session.clientIp, clientIp: session.clientIp,
browser: session.browser, browser: session.browser,
browserVersion: session.browserVersion, browserVersion: session.browserVersion,

View File

@ -13,6 +13,118 @@ msgstr ""
"Language-Team: \n" "Language-Team: \n"
"Plural-Forms: \n" "Plural-Forms: \n"
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
#: public/app/features/profile/UserProfileEditForm.tsx #: public/app/features/profile/UserProfileEditForm.tsx
msgid "edit-user-profile.title" msgid "common.save"
msgstr "Save"
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
msgid "shared-dashboard.fields.timezone-label"
msgstr "Timezone"
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
msgid "shared-preferences.fields.home-dashboard-label"
msgstr "Home Dashboard"
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
msgid "shared-preferences.fields.home-dashboard-placeholder"
msgstr "Choose default dashboard"
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
msgid "shared-preferences.fields.home-dashboard-tooltip"
msgstr "Not finding the dashboard you want? Star it first, then it should appear in this select box."
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
msgid "shared-preferences.fields.theme-label"
msgstr "UI Theme"
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
msgid "shared-preferences.fields.week-start-label"
msgstr "Week start"
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
msgid "shared-preferences.theme.dark-label"
msgstr "Dark"
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
msgid "shared-preferences.theme.default-label"
msgstr "Default"
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
msgid "shared-preferences.theme.light-label"
msgstr "Light"
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
msgid "shared-preferences.title"
msgstr "Preferences"
#: public/app/features/profile/UserOrganizations.tsx
msgid "user-orgs.current-org-button"
msgstr "Current"
#: public/app/features/profile/UserOrganizations.tsx
msgid "user-orgs.name-column"
msgstr "Name"
#: public/app/features/profile/UserOrganizations.tsx
msgid "user-orgs.role-column"
msgstr "Role"
#: public/app/features/profile/UserOrganizations.tsx
msgid "user-orgs.select-org-button"
msgstr "Select"
#: public/app/features/profile/UserOrganizations.tsx
msgid "user-orgs.title"
msgstr "Organizations"
#: public/app/features/profile/UserProfileEditForm.tsx
msgid "user-profile.fields.email-error"
msgstr "Email is required"
#: public/app/features/profile/UserProfileEditForm.tsx
#: public/app/features/profile/UserProfileEditForm.tsx
msgid "user-profile.fields.email-label"
msgstr "Email"
#: public/app/features/profile/UserProfileEditForm.tsx
msgid "user-profile.fields.name-error"
msgstr "Name is required"
#: public/app/features/profile/UserProfileEditForm.tsx
#: public/app/features/profile/UserProfileEditForm.tsx
msgid "user-profile.fields.name-label"
msgstr "Name"
#: public/app/features/profile/UserProfileEditForm.tsx
#: public/app/features/profile/UserProfileEditForm.tsx
msgid "user-profile.fields.username-label"
msgstr "Username"
#: public/app/features/profile/UserProfileEditForm.tsx
msgid "user-profile.title"
msgstr "Edit profile" msgstr "Edit profile"
#: public/app/features/profile/UserSessions.tsx
msgid "user-session.browser-column"
msgstr "Browser & OS"
#: public/app/features/profile/UserSessions.tsx
msgid "user-session.created-at-column"
msgstr "Logged on"
#: public/app/features/profile/UserSessions.tsx
msgid "user-session.ip-column"
msgstr "IP address"
#: public/app/features/profile/UserSessions.tsx
msgid "user-session.revoke"
msgstr "Revoke user session"
#: public/app/features/profile/UserSessions.tsx
msgid "user-session.seen-at-column"
msgstr "Last seen"
#: public/app/features/profile/UserSessions.tsx
msgid "user-sessions.loading"
msgstr "Loading sessions..."

View File

@ -13,6 +13,118 @@ msgstr ""
"Language-Team: \n" "Language-Team: \n"
"Plural-Forms: \n" "Plural-Forms: \n"
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
#: public/app/features/profile/UserProfileEditForm.tsx #: public/app/features/profile/UserProfileEditForm.tsx
msgid "edit-user-profile.title" msgid "common.save"
msgstr ""
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
msgid "shared-dashboard.fields.timezone-label"
msgstr ""
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
msgid "shared-preferences.fields.home-dashboard-label"
msgstr ""
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
msgid "shared-preferences.fields.home-dashboard-placeholder"
msgstr ""
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
msgid "shared-preferences.fields.home-dashboard-tooltip"
msgstr ""
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
msgid "shared-preferences.fields.theme-label"
msgstr ""
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
msgid "shared-preferences.fields.week-start-label"
msgstr ""
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
msgid "shared-preferences.theme.dark-label"
msgstr ""
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
msgid "shared-preferences.theme.default-label"
msgstr ""
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
msgid "shared-preferences.theme.light-label"
msgstr ""
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
msgid "shared-preferences.title"
msgstr ""
#: public/app/features/profile/UserOrganizations.tsx
msgid "user-orgs.current-org-button"
msgstr ""
#: public/app/features/profile/UserOrganizations.tsx
msgid "user-orgs.name-column"
msgstr ""
#: public/app/features/profile/UserOrganizations.tsx
msgid "user-orgs.role-column"
msgstr ""
#: public/app/features/profile/UserOrganizations.tsx
msgid "user-orgs.select-org-button"
msgstr ""
#: public/app/features/profile/UserOrganizations.tsx
msgid "user-orgs.title"
msgstr ""
#: public/app/features/profile/UserProfileEditForm.tsx
msgid "user-profile.fields.email-error"
msgstr ""
#: public/app/features/profile/UserProfileEditForm.tsx
#: public/app/features/profile/UserProfileEditForm.tsx
msgid "user-profile.fields.email-label"
msgstr ""
#: public/app/features/profile/UserProfileEditForm.tsx
msgid "user-profile.fields.name-error"
msgstr ""
#: public/app/features/profile/UserProfileEditForm.tsx
#: public/app/features/profile/UserProfileEditForm.tsx
msgid "user-profile.fields.name-label"
msgstr ""
#: public/app/features/profile/UserProfileEditForm.tsx
#: public/app/features/profile/UserProfileEditForm.tsx
msgid "user-profile.fields.username-label"
msgstr ""
#: public/app/features/profile/UserProfileEditForm.tsx
msgid "user-profile.title"
msgstr "Editar perfil" msgstr "Editar perfil"
#: public/app/features/profile/UserSessions.tsx
msgid "user-session.browser-column"
msgstr ""
#: public/app/features/profile/UserSessions.tsx
msgid "user-session.created-at-column"
msgstr ""
#: public/app/features/profile/UserSessions.tsx
msgid "user-session.ip-column"
msgstr ""
#: public/app/features/profile/UserSessions.tsx
msgid "user-session.revoke"
msgstr ""
#: public/app/features/profile/UserSessions.tsx
msgid "user-session.seen-at-column"
msgstr ""
#: public/app/features/profile/UserSessions.tsx
msgid "user-sessions.loading"
msgstr ""

View File

@ -13,6 +13,118 @@ msgstr ""
"Language-Team: \n" "Language-Team: \n"
"Plural-Forms: \n" "Plural-Forms: \n"
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
#: public/app/features/profile/UserProfileEditForm.tsx #: public/app/features/profile/UserProfileEditForm.tsx
msgid "edit-user-profile.title" msgid "common.save"
msgstr ""
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
msgid "shared-dashboard.fields.timezone-label"
msgstr ""
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
msgid "shared-preferences.fields.home-dashboard-label"
msgstr ""
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
msgid "shared-preferences.fields.home-dashboard-placeholder"
msgstr ""
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
msgid "shared-preferences.fields.home-dashboard-tooltip"
msgstr ""
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
msgid "shared-preferences.fields.theme-label"
msgstr ""
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
msgid "shared-preferences.fields.week-start-label"
msgstr ""
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
msgid "shared-preferences.theme.dark-label"
msgstr ""
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
msgid "shared-preferences.theme.default-label"
msgstr ""
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
msgid "shared-preferences.theme.light-label"
msgstr ""
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
msgid "shared-preferences.title"
msgstr ""
#: public/app/features/profile/UserOrganizations.tsx
msgid "user-orgs.current-org-button"
msgstr ""
#: public/app/features/profile/UserOrganizations.tsx
msgid "user-orgs.name-column"
msgstr ""
#: public/app/features/profile/UserOrganizations.tsx
msgid "user-orgs.role-column"
msgstr ""
#: public/app/features/profile/UserOrganizations.tsx
msgid "user-orgs.select-org-button"
msgstr ""
#: public/app/features/profile/UserOrganizations.tsx
msgid "user-orgs.title"
msgstr ""
#: public/app/features/profile/UserProfileEditForm.tsx
msgid "user-profile.fields.email-error"
msgstr ""
#: public/app/features/profile/UserProfileEditForm.tsx
#: public/app/features/profile/UserProfileEditForm.tsx
msgid "user-profile.fields.email-label"
msgstr ""
#: public/app/features/profile/UserProfileEditForm.tsx
msgid "user-profile.fields.name-error"
msgstr ""
#: public/app/features/profile/UserProfileEditForm.tsx
#: public/app/features/profile/UserProfileEditForm.tsx
msgid "user-profile.fields.name-label"
msgstr ""
#: public/app/features/profile/UserProfileEditForm.tsx
#: public/app/features/profile/UserProfileEditForm.tsx
msgid "user-profile.fields.username-label"
msgstr ""
#: public/app/features/profile/UserProfileEditForm.tsx
msgid "user-profile.title"
msgstr "Editer le profil" msgstr "Editer le profil"
#: public/app/features/profile/UserSessions.tsx
msgid "user-session.browser-column"
msgstr ""
#: public/app/features/profile/UserSessions.tsx
msgid "user-session.created-at-column"
msgstr ""
#: public/app/features/profile/UserSessions.tsx
msgid "user-session.ip-column"
msgstr ""
#: public/app/features/profile/UserSessions.tsx
msgid "user-session.revoke"
msgstr ""
#: public/app/features/profile/UserSessions.tsx
msgid "user-session.seen-at-column"
msgstr ""
#: public/app/features/profile/UserSessions.tsx
msgid "user-sessions.loading"
msgstr ""

View File

@ -0,0 +1,130 @@
msgid ""
msgstr ""
"POT-Creation-Date: 2022-01-10 10:42+0000\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: @lingui/cli\n"
"Language: pseudo-LOCALE\n"
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
"Plural-Forms: \n"
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
#: public/app/features/profile/UserProfileEditForm.tsx
msgid "common.save"
msgstr ""
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
msgid "shared-dashboard.fields.timezone-label"
msgstr ""
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
msgid "shared-preferences.fields.home-dashboard-label"
msgstr ""
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
msgid "shared-preferences.fields.home-dashboard-placeholder"
msgstr ""
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
msgid "shared-preferences.fields.home-dashboard-tooltip"
msgstr ""
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
msgid "shared-preferences.fields.theme-label"
msgstr ""
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
msgid "shared-preferences.fields.week-start-label"
msgstr ""
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
msgid "shared-preferences.theme.dark-label"
msgstr ""
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
msgid "shared-preferences.theme.default-label"
msgstr ""
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
msgid "shared-preferences.theme.light-label"
msgstr ""
#: public/app/core/components/SharedPreferences/SharedPreferences.tsx
msgid "shared-preferences.title"
msgstr ""
#: public/app/features/profile/UserOrganizations.tsx
msgid "user-orgs.current-org-button"
msgstr ""
#: public/app/features/profile/UserOrganizations.tsx
msgid "user-orgs.name-column"
msgstr ""
#: public/app/features/profile/UserOrganizations.tsx
msgid "user-orgs.role-column"
msgstr ""
#: public/app/features/profile/UserOrganizations.tsx
msgid "user-orgs.select-org-button"
msgstr ""
#: public/app/features/profile/UserOrganizations.tsx
msgid "user-orgs.title"
msgstr ""
#: public/app/features/profile/UserProfileEditForm.tsx
msgid "user-profile.fields.email-error"
msgstr ""
#: public/app/features/profile/UserProfileEditForm.tsx
#: public/app/features/profile/UserProfileEditForm.tsx
msgid "user-profile.fields.email-label"
msgstr ""
#: public/app/features/profile/UserProfileEditForm.tsx
msgid "user-profile.fields.name-error"
msgstr ""
#: public/app/features/profile/UserProfileEditForm.tsx
#: public/app/features/profile/UserProfileEditForm.tsx
msgid "user-profile.fields.name-label"
msgstr ""
#: public/app/features/profile/UserProfileEditForm.tsx
#: public/app/features/profile/UserProfileEditForm.tsx
msgid "user-profile.fields.username-label"
msgstr ""
#: public/app/features/profile/UserProfileEditForm.tsx
msgid "user-profile.title"
msgstr ""
#: public/app/features/profile/UserSessions.tsx
msgid "user-session.browser-column"
msgstr ""
#: public/app/features/profile/UserSessions.tsx
msgid "user-session.created-at-column"
msgstr ""
#: public/app/features/profile/UserSessions.tsx
msgid "user-session.ip-column"
msgstr ""
#: public/app/features/profile/UserSessions.tsx
msgid "user-session.revoke"
msgstr ""
#: public/app/features/profile/UserSessions.tsx
msgid "user-session.seen-at-column"
msgstr ""
#: public/app/features/profile/UserSessions.tsx
msgid "user-sessions.loading"
msgstr ""

View File

@ -0,0 +1,8 @@
import React from 'react';
import { I18nProvider } from '../../app/core/localisation';
const TestProvider: React.FC = ({ children }) => {
return <I18nProvider>{children}</I18nProvider>;
};
export default TestProvider;

View File

@ -15,3 +15,7 @@ export const Select: React.FC = () => {
export const SelectOrdinal: React.FC = () => { export const SelectOrdinal: React.FC = () => {
throw new Error('SelectOrdinal mock not implemented yet'); throw new Error('SelectOrdinal mock not implemented yet');
}; };
export const t = (msg: string | { message: string }) => {
return typeof msg === 'string' ? msg : msg.message;
};