@@ -69,6 +69,7 @@ export default class UserTokensMock { | |||
login, | |||
type, | |||
projectKey, | |||
isExpired: false, | |||
token: Math.random() | |||
.toString(RANDOM_RADIX) | |||
.slice(RANDOM_PREFIX), |
@@ -24,6 +24,7 @@ import selectEvent from 'react-select-event'; | |||
import { getMyProjects, getScannableProjects } from '../../../api/components'; | |||
import NotificationsMock from '../../../api/mocks/NotificationsMock'; | |||
import UserTokensMock from '../../../api/mocks/UserTokensMock'; | |||
import { mockUserToken } from '../../../helpers/mocks/token'; | |||
import { mockCurrentUser, mockLoggedInUser } from '../../../helpers/testMocks'; | |||
import { renderApp } from '../../../helpers/testReactTestingUtils'; | |||
import { Permissions } from '../../../types/permissions'; | |||
@@ -295,6 +296,33 @@ describe('security page', () => { | |||
} | |||
); | |||
it('should flag expired tokens as such', async () => { | |||
tokenMock.tokens.push( | |||
mockUserToken({ | |||
name: 'expired token', | |||
isExpired: true, | |||
expirationDate: '2021-01-23T19:25:19+0000' | |||
}) | |||
); | |||
renderAccountApp( | |||
mockLoggedInUser({ permissions: { global: [Permissions.Scan] } }), | |||
securityPagePath | |||
); | |||
expect(await screen.findByText('users.tokens')).toBeInTheDocument(); | |||
// expired token is flagged as such | |||
const expiredTokenRow = screen.getByRole('row', { name: /expired token/ }); | |||
expect(within(expiredTokenRow).getByText('my_account.tokens.expired')).toBeInTheDocument(); | |||
// unexpired token is not flagged | |||
const unexpiredTokenRow = screen.getAllByRole('row')[0]; | |||
expect( | |||
within(unexpiredTokenRow).queryByText('my_account.tokens.expired') | |||
).not.toBeInTheDocument(); | |||
}); | |||
it("should not suggest creating a Project token if the user doesn't have at least one scannable Projects", async () => { | |||
(getScannableProjects as jest.Mock).mockResolvedValueOnce({ | |||
projects: [] |
@@ -18,7 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
.account-container { | |||
width: 800px; | |||
width: 1000px; | |||
margin-left: auto; | |||
margin-right: auto; | |||
} | |||
@@ -63,11 +63,6 @@ | |||
border-top: 1px solid var(--barBorderColor); | |||
} | |||
.account-projects-list { | |||
margin-left: -20px; | |||
margin-right: -20px; | |||
} | |||
.account-projects-list > li { | |||
padding: 15px 20px; | |||
} |
@@ -127,6 +127,7 @@ export class TokensForm extends React.PureComponent<Props, State> { | |||
{ | |||
name: newToken.name, | |||
createdAt: newToken.createdAt, | |||
isExpired: false, | |||
type: newTokenType, | |||
...(newTokenType === TokenType.Project && { | |||
project: { key: selectedProject.key, name: selectedProject.name } | |||
@@ -254,7 +255,7 @@ export class TokensForm extends React.PureComponent<Props, State> { | |||
if (tokens.length <= 0) { | |||
return ( | |||
<tr> | |||
<td className="note" colSpan={3}> | |||
<td className="note" colSpan={7}> | |||
{translate('users.no_tokens')} | |||
</td> | |||
</tr> | |||
@@ -295,7 +296,8 @@ export class TokensForm extends React.PureComponent<Props, State> { | |||
<th>{translate('my_account.project_name')}</th> | |||
<th>{translate('my_account.tokens_last_usage')}</th> | |||
<th className="text-right">{translate('created')}</th> | |||
<th /> | |||
<th className="text-right">{translate('my_account.tokens.expiration')}</th> | |||
<th aria-label={translate('actions')} /> | |||
</tr> | |||
</thead> | |||
<tbody> |
@@ -17,11 +17,13 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import { revokeToken } from '../../../api/user-tokens'; | |||
import { Button } from '../../../components/controls/buttons'; | |||
import ConfirmButton from '../../../components/controls/ConfirmButton'; | |||
import WarningIcon from '../../../components/icons/WarningIcon'; | |||
import DateFormatter from '../../../components/intl/DateFormatter'; | |||
import DateFromNow from '../../../components/intl/DateFromNow'; | |||
import DeferredSpinner from '../../../components/ui/DeferredSpinner'; | |||
@@ -82,9 +84,15 @@ export default class TokensFormItem extends React.PureComponent<Props, State> { | |||
const { deleteConfirmation, token } = this.props; | |||
const { loading, showConfirmation } = this.state; | |||
return ( | |||
<tr> | |||
<tr className={classNames({ 'text-muted-2': token.isExpired })}> | |||
<td title={token.name} className="hide-overflow nowrap"> | |||
{token.name} | |||
{token.isExpired && ( | |||
<div className="spacer-top text-warning"> | |||
<WarningIcon className="little-spacer-right" /> | |||
{translate('my_account.tokens.expired')} | |||
</div> | |||
)} | |||
</td> | |||
<td title={translate('users.tokens', token.type)} className="hide-overflow thin"> | |||
{translate('users.tokens', token.type, 'short')} | |||
@@ -98,6 +106,9 @@ export default class TokensFormItem extends React.PureComponent<Props, State> { | |||
<td className="thin nowrap text-right"> | |||
<DateFormatter date={token.createdAt} long={true} /> | |||
</td> | |||
<td className={classNames('thin nowrap text-right', { 'text-warning': token.isExpired })}> | |||
{token.expirationDate ? <DateFormatter date={token.expirationDate} long={true} /> : '–'} | |||
</td> | |||
<td className="thin nowrap text-right"> | |||
{deleteConfirmation === 'modal' ? ( | |||
<ConfirmButton |
@@ -50,7 +50,14 @@ exports[`should render correctly 1`] = ` | |||
> | |||
created | |||
</th> | |||
<th /> | |||
<th | |||
className="text-right" | |||
> | |||
my_account.tokens.expiration | |||
</th> | |||
<th | |||
aria-label="actions" | |||
/> | |||
</tr> | |||
</thead> | |||
<tbody> | |||
@@ -69,7 +76,7 @@ exports[`should render correctly 1`] = ` | |||
<tr> | |||
<td | |||
className="note" | |||
colSpan={3} | |||
colSpan={7} | |||
> | |||
users.no_tokens | |||
</td> | |||
@@ -130,7 +137,14 @@ exports[`should render correctly 2`] = ` | |||
> | |||
created | |||
</th> | |||
<th /> | |||
<th | |||
className="text-right" | |||
> | |||
my_account.tokens.expiration | |||
</th> | |||
<th | |||
aria-label="actions" | |||
/> | |||
</tr> | |||
</thead> | |||
<tbody> |
@@ -1,7 +1,9 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<tr> | |||
<tr | |||
className="" | |||
> | |||
<td | |||
className="hide-overflow nowrap" | |||
title="foo" | |||
@@ -33,6 +35,11 @@ exports[`should render correctly 1`] = ` | |||
long={true} | |||
/> | |||
</td> | |||
<td | |||
className="thin nowrap text-right" | |||
> | |||
– | |||
</td> | |||
<td | |||
className="thin nowrap text-right" | |||
> | |||
@@ -52,7 +59,9 @@ exports[`should render correctly 1`] = ` | |||
`; | |||
exports[`should render correctly 2`] = ` | |||
<tr> | |||
<tr | |||
className="" | |||
> | |||
<td | |||
className="hide-overflow nowrap" | |||
title="foo" | |||
@@ -84,6 +93,11 @@ exports[`should render correctly 2`] = ` | |||
long={true} | |||
/> | |||
</td> | |||
<td | |||
className="thin nowrap text-right" | |||
> | |||
– | |||
</td> | |||
<td | |||
className="thin nowrap text-right" | |||
> |
@@ -25,6 +25,7 @@ export function mockUserToken(overrides: Partial<UserToken> = {}): UserToken { | |||
name: 'Token name', | |||
createdAt: '2019-06-14T09:45:52+0200', | |||
type: TokenType.User, | |||
isExpired: false, | |||
...overrides | |||
}; | |||
} |
@@ -28,6 +28,8 @@ export interface UserToken { | |||
name: string; | |||
createdAt: string; | |||
lastConnectionDate?: string; | |||
expirationDate?: string; | |||
isExpired: boolean; | |||
type: TokenType; | |||
project?: { name: string; key: string }; | |||
} |
@@ -2027,6 +2027,8 @@ my_account.tokens_description=If you want to enforce security by not providing c | |||
my_account.token_type=Type | |||
my_account.project_name=Project | |||
my_account.tokens_last_usage=Last use | |||
my_account.tokens.expiration=Expiration | |||
my_account.tokens.expired=Token is expired | |||
my_account.projects=Projects | |||
my_account.projects.description=Those projects are the ones you are administering. | |||
my_account.projects.no_results=You are not administering any project yet. |