aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGuillaume Peoc'h <guillaume.peoch@sonarsource.com>2022-06-29 17:31:44 +0200
committersonartech <sonartech@sonarsource.com>2022-07-05 20:02:54 +0000
commitbb411d21619c2aeadcd31df5ab1f68e8d819ac09 (patch)
treef2194908d9ffea7e914dde2b16a7fde09a2392e4
parenta2d4cdc405dd84cfaa76ec29b6bad6e12358cdfd (diff)
downloadsonarqube-bb411d21619c2aeadcd31df5ab1f68e8d819ac09.tar.gz
sonarqube-bb411d21619c2aeadcd31df5ab1f68e8d819ac09.zip
SONAR-16565 Allow users to select the token expiration interval
-rw-r--r--server/sonar-web/src/main/js/api/user-tokens.ts1
-rw-r--r--server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx57
-rw-r--r--server/sonar-web/src/main/js/apps/audit-logs/components/AuditAppRenderer.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/audit-logs/components/DownloadButton.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditAppRenderer-test.tsx12
-rw-r--r--server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/DownloadButton-test.tsx10
-rw-r--r--server/sonar-web/src/main/js/apps/audit-logs/utils.ts4
-rw-r--r--server/sonar-web/src/main/js/apps/users/components/TokensForm.tsx134
-rw-r--r--server/sonar-web/src/main/js/apps/users/components/__tests__/TokensForm-test.tsx97
-rw-r--r--server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensForm-test.tsx.snap192
-rw-r--r--server/sonar-web/src/main/js/helpers/dates.ts4
-rw-r--r--server/sonar-web/src/main/js/types/token.ts7
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties16
13 files changed, 193 insertions, 347 deletions
diff --git a/server/sonar-web/src/main/js/api/user-tokens.ts b/server/sonar-web/src/main/js/api/user-tokens.ts
index 1cb261c2a8e..c016d4452a3 100644
--- a/server/sonar-web/src/main/js/api/user-tokens.ts
+++ b/server/sonar-web/src/main/js/api/user-tokens.ts
@@ -31,6 +31,7 @@ export function generateToken(data: {
projectKey?: string;
type?: string;
login?: string;
+ expirationDate?: string;
}): Promise<NewUserToken> {
return postJSON('/api/user_tokens/generate', data).catch(throwGlobalError);
}
diff --git a/server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx b/server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx
index 2d0a7c4bf87..62eb0091e8b 100644
--- a/server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx
+++ b/server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx
@@ -24,17 +24,25 @@ 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 { generateToken } from '../../../api/user-tokens';
import { mockUserToken } from '../../../helpers/mocks/token';
import { mockCurrentUser, mockLoggedInUser } from '../../../helpers/testMocks';
import { renderApp } from '../../../helpers/testReactTestingUtils';
import { Permissions } from '../../../types/permissions';
-import { TokenType } from '../../../types/token';
+import { TokenExpiration, TokenType } from '../../../types/token';
import { CurrentUser } from '../../../types/users';
import routes from '../routes';
jest.mock('../../../api/user-tokens');
jest.mock('../../../api/notifications');
+jest.mock('../../../helpers/dates', () => {
+ return {
+ ...jest.requireActual('../../../helpers/dates'),
+ now: jest.fn(() => new Date('2022-06-01T12:00:00Z'))
+ };
+});
+
jest.mock('../../../api/components', () => ({
getMyProjects: jest.fn().mockResolvedValue({
paging: { total: 2, pageIndex: 1, pageSize: 10 },
@@ -207,6 +215,7 @@ describe('profile page', () => {
describe('security page', () => {
let tokenMock: UserTokensMock;
+
beforeAll(() => {
tokenMock = new UserTokensMock();
});
@@ -218,6 +227,48 @@ describe('security page', () => {
const securityPagePath = 'account/security';
it.each([
+ [TokenExpiration.OneMonth, '2022-07-01'],
+ [TokenExpiration.ThreeMonths, '2022-08-30'],
+ [TokenExpiration.OneYear, '2023-06-01'],
+ [TokenExpiration.NoExpiration, undefined]
+ ])(
+ 'should allow token creation with proper expiration date for %s days',
+ async (tokenExpirationTime, expectedTime) => {
+ const user = userEvent.setup();
+
+ renderAccountApp(
+ mockLoggedInUser({ permissions: { global: [Permissions.Scan] } }),
+ securityPagePath
+ );
+
+ // Add the token
+ const newTokenName = 'importantToken' + tokenExpirationTime;
+ const input = screen.getByPlaceholderText('users.tokens.enter_name');
+ const generateButton = screen.getByRole('button', { name: 'users.generate' });
+ expect(input).toBeInTheDocument();
+ await user.click(input);
+ await user.keyboard(newTokenName);
+
+ expect(generateButton).toBeDisabled();
+
+ const tokenTypeLabel = `users.tokens.${TokenType.User}`;
+ await selectEvent.select(screen.getAllByRole('textbox')[1], [tokenTypeLabel]);
+
+ const tokenExpirationLabel = `users.tokens.expiration.${tokenExpirationTime}`;
+ await selectEvent.select(screen.getAllByRole('textbox')[2], [tokenExpirationLabel]);
+
+ await user.click(generateButton);
+
+ expect(generateToken).toHaveBeenLastCalledWith({
+ name: newTokenName,
+ login: 'luke',
+ type: TokenType.User,
+ expirationDate: expectedTime
+ });
+ }
+ );
+
+ it.each([
['user', TokenType.User],
['global', TokenType.Global],
['project analysis', TokenType.Project]
@@ -236,7 +287,7 @@ describe('security page', () => {
// Add the token
const newTokenName = 'importantToken';
- const input = screen.getByPlaceholderText('users.enter_token_name');
+ const input = screen.getByPlaceholderText('users.tokens.enter_name');
const generateButton = screen.getByRole('button', { name: 'users.generate' });
expect(input).toBeInTheDocument();
await user.click(input);
@@ -250,7 +301,7 @@ describe('security page', () => {
if (tokenTypeOption === TokenType.Project) {
await selectEvent.select(screen.getAllByRole('textbox')[1], [tokenTypeLabel]);
expect(generateButton).toBeDisabled();
- expect(screen.getAllByRole('textbox')).toHaveLength(3);
+ expect(screen.getAllByRole('textbox')).toHaveLength(4);
await selectEvent.select(screen.getAllByRole('textbox')[2], ['Project Name 1']);
expect(generateButton).not.toBeDisabled();
} else {
diff --git a/server/sonar-web/src/main/js/apps/audit-logs/components/AuditAppRenderer.tsx b/server/sonar-web/src/main/js/apps/audit-logs/components/AuditAppRenderer.tsx
index c3f1952a905..38901794aac 100644
--- a/server/sonar-web/src/main/js/apps/audit-logs/components/AuditAppRenderer.tsx
+++ b/server/sonar-web/src/main/js/apps/audit-logs/components/AuditAppRenderer.tsx
@@ -25,10 +25,11 @@ import { Link } from 'react-router-dom';
import DateRangeInput from '../../../components/controls/DateRangeInput';
import Radio from '../../../components/controls/Radio';
import Suggestions from '../../../components/embed-docs-modal/Suggestions';
+import { now } from '../../../helpers/dates';
import { translate } from '../../../helpers/l10n';
import { queryToSearch } from '../../../helpers/urls';
import '../style.css';
-import { HousekeepingPolicy, now, RangeOption } from '../utils';
+import { HousekeepingPolicy, RangeOption } from '../utils';
import DownloadButton from './DownloadButton';
export interface AuditAppRendererProps {
diff --git a/server/sonar-web/src/main/js/apps/audit-logs/components/DownloadButton.tsx b/server/sonar-web/src/main/js/apps/audit-logs/components/DownloadButton.tsx
index 4dd12270c99..ab00e68fbea 100644
--- a/server/sonar-web/src/main/js/apps/audit-logs/components/DownloadButton.tsx
+++ b/server/sonar-web/src/main/js/apps/audit-logs/components/DownloadButton.tsx
@@ -20,10 +20,11 @@
import classNames from 'classnames';
import { endOfDay, startOfDay, subDays } from 'date-fns';
import * as React from 'react';
+import { now } from '../../../helpers/dates';
import { translate } from '../../../helpers/l10n';
import { getBaseUrl } from '../../../helpers/system';
import '../style.css';
-import { now, RangeOption } from '../utils';
+import { RangeOption } from '../utils';
export interface DownloadButtonProps {
dateRange?: { from?: Date; to?: Date };
diff --git a/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditAppRenderer-test.tsx b/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditAppRenderer-test.tsx
index 3a2bdaf6c24..d28fbb03aa8 100644
--- a/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditAppRenderer-test.tsx
+++ b/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/AuditAppRenderer-test.tsx
@@ -19,17 +19,14 @@
*/
import { shallow } from 'enzyme';
import * as React from 'react';
+import { now } from '../../../../helpers/dates';
import { HousekeepingPolicy, RangeOption } from '../../utils';
import AuditAppRenderer, { AuditAppRendererProps } from '../AuditAppRenderer';
-jest.mock('../../utils', () => {
- const { HousekeepingPolicy, RangeOption } = jest.requireActual('../../utils');
- const now = new Date('2020-07-21T12:00:00Z');
-
+jest.mock('../../../../helpers/dates', () => {
return {
- HousekeepingPolicy,
- now: jest.fn().mockReturnValue(now),
- RangeOption
+ ...jest.requireActual('../../../../helpers/dates'),
+ now: jest.fn(() => new Date('2020-07-21T12:00:00Z'))
};
});
@@ -40,6 +37,7 @@ it.each([
[HousekeepingPolicy.Yearly]
])('should render correctly for %s housekeeping policy', housekeepingPolicy => {
expect(shallowRender({ housekeepingPolicy })).toMatchSnapshot();
+ now();
});
function shallowRender(props: Partial<AuditAppRendererProps> = {}) {
diff --git a/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/DownloadButton-test.tsx b/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/DownloadButton-test.tsx
index 867c539c29c..27b74dbf321 100644
--- a/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/DownloadButton-test.tsx
+++ b/server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/DownloadButton-test.tsx
@@ -32,14 +32,10 @@ jest.mock('date-fns', () => {
};
});
-jest.mock('../../utils', () => {
- const { HousekeepingPolicy, RangeOption } = jest.requireActual('../../utils');
- const now = new Date('2020-07-21T12:00:00Z');
-
+jest.mock('../../../../helpers/dates', () => {
return {
- HousekeepingPolicy,
- now: jest.fn().mockReturnValue(now),
- RangeOption
+ ...jest.requireActual('../../../../helpers/dates'),
+ now: jest.fn(() => new Date('2020-07-21T12:00:00Z'))
};
});
diff --git a/server/sonar-web/src/main/js/apps/audit-logs/utils.ts b/server/sonar-web/src/main/js/apps/audit-logs/utils.ts
index 3cf47aa06dd..b59b65ff7ec 100644
--- a/server/sonar-web/src/main/js/apps/audit-logs/utils.ts
+++ b/server/sonar-web/src/main/js/apps/audit-logs/utils.ts
@@ -31,7 +31,3 @@ export enum RangeOption {
Trimester = '90days',
Custom = 'custom'
}
-
-export function now() {
- return new Date();
-}
diff --git a/server/sonar-web/src/main/js/apps/users/components/TokensForm.tsx b/server/sonar-web/src/main/js/apps/users/components/TokensForm.tsx
index 6f32d5f24e9..e16327a2a43 100644
--- a/server/sonar-web/src/main/js/apps/users/components/TokensForm.tsx
+++ b/server/sonar-web/src/main/js/apps/users/components/TokensForm.tsx
@@ -25,10 +25,11 @@ import withCurrentUserContext from '../../../app/components/current-user/withCur
import { SubmitButton } from '../../../components/controls/buttons';
import Select, { BasicSelectOption } from '../../../components/controls/Select';
import DeferredSpinner from '../../../components/ui/DeferredSpinner';
+import { now, toShortNotSoISOString } from '../../../helpers/dates';
import { translate } from '../../../helpers/l10n';
import { hasGlobalPermission } from '../../../helpers/users';
import { Permissions } from '../../../types/permissions';
-import { TokenType, UserToken } from '../../../types/token';
+import { TokenExpiration, TokenType, UserToken } from '../../../types/token';
import { CurrentUser } from '../../../types/users';
import TokensFormItem, { TokenDeleteConfirmation } from './TokensFormItem';
import TokensFormNewToken from './TokensFormNewToken';
@@ -50,8 +51,21 @@ interface State {
tokens: UserToken[];
projects: BasicSelectOption[];
selectedProject: { key: string; name: string };
+ newTokenExpiration: TokenExpiration;
}
+const EXPIRATION_OPTIONS = [
+ TokenExpiration.OneMonth,
+ TokenExpiration.ThreeMonths,
+ TokenExpiration.OneYear,
+ TokenExpiration.NoExpiration
+].map(value => {
+ return {
+ value,
+ label: translate('users.tokens.expiration', value.toString())
+ };
+});
+
export class TokensForm extends React.PureComponent<Props, State> {
mounted = false;
state: State = {
@@ -61,7 +75,8 @@ export class TokensForm extends React.PureComponent<Props, State> {
newTokenType: this.props.displayTokenTypeInput ? undefined : TokenType.User,
selectedProject: { key: '', name: '' },
tokens: [],
- projects: []
+ projects: [],
+ newTokenExpiration: TokenExpiration.OneMonth
};
componentDidMount() {
@@ -106,10 +121,21 @@ export class TokensForm extends React.PureComponent<Props, State> {
}
};
+ getExpirationDate = (days: number): string => {
+ const expirationDate = now();
+ expirationDate.setDate(expirationDate.getDate() + days);
+ return toShortNotSoISOString(expirationDate);
+ };
+
handleGenerateToken = async (event: React.SyntheticEvent<HTMLFormElement>) => {
event.preventDefault();
const { login } = this.props;
- const { newTokenName, newTokenType = TokenType.User, selectedProject } = this.state;
+ const {
+ newTokenName,
+ newTokenType = TokenType.User,
+ selectedProject,
+ newTokenExpiration
+ } = this.state;
this.setState({ generating: true });
try {
@@ -117,7 +143,10 @@ export class TokensForm extends React.PureComponent<Props, State> {
name: newTokenName,
login,
type: newTokenType,
- ...(newTokenType === TokenType.Project && { projectKey: selectedProject.key })
+ ...(newTokenType === TokenType.Project && { projectKey: selectedProject.key }),
+ ...(newTokenExpiration !== TokenExpiration.NoExpiration && {
+ expirationDate: this.getExpirationDate(newTokenExpiration)
+ })
});
if (this.mounted) {
@@ -190,8 +219,18 @@ export class TokensForm extends React.PureComponent<Props, State> {
this.setState({ selectedProject: { key: value, name: label } });
};
+ handleNewTokenExpirationChange = ({ value }: { value: TokenExpiration }) => {
+ this.setState({ newTokenExpiration: value });
+ };
+
renderForm() {
- const { newTokenName, newTokenType, projects, selectedProject } = this.state;
+ const {
+ newTokenName,
+ newTokenType,
+ projects,
+ selectedProject,
+ newTokenExpiration
+ } = this.state;
const { displayTokenTypeInput, currentUser } = this.props;
const tokenTypeOptions = [
@@ -212,38 +251,71 @@ export class TokensForm extends React.PureComponent<Props, State> {
return (
<form autoComplete="off" className="display-flex-center" onSubmit={this.handleGenerateToken}>
- <input
- className="input-large spacer-right it__token-name"
- maxLength={100}
- onChange={this.handleNewTokenChange}
- placeholder={translate('users.enter_token_name')}
- required={true}
- type="text"
- value={newTokenName}
- />
+ <div className="display-flex-column input-large spacer-right ">
+ <label htmlFor="token-name" className="text-bold">
+ {translate('users.tokens.name')}
+ </label>
+ <input
+ id="token-name"
+ className="spacer-top it__token-name"
+ maxLength={100}
+ onChange={this.handleNewTokenChange}
+ placeholder={translate('users.tokens.enter_name')}
+ required={true}
+ type="text"
+ value={newTokenName}
+ />
+ </div>
{displayTokenTypeInput && (
<>
- <Select
- className="input-large spacer-right it__token-type"
- isSearchable={false}
- onChange={this.handleNewTokenTypeChange}
- options={tokenTypeOptions}
- placeholder={translate('users.select_token_type')}
- value={tokenTypeOptions.find(option => option.value === newTokenType) || null}
- />
- {newTokenType === TokenType.Project && (
+ <div className="display-flex-column input-large spacer-right">
+ <label htmlFor="token-select-type" className="text-bold">
+ {translate('users.tokens.type')}
+ </label>
<Select
- className="input-large spacer-right it__project"
- onChange={this.handleProjectChange}
- options={projects}
- placeholder={translate('users.select_token_project')}
- value={projects.find(project => project.value === selectedProject.key)}
+ id="token-select-type"
+ className="spacer-top it__token-type"
+ isSearchable={false}
+ onChange={this.handleNewTokenTypeChange}
+ options={tokenTypeOptions}
+ placeholder={translate('users.tokens.select_type')}
+ value={tokenTypeOptions.find(option => option.value === newTokenType) || null}
/>
+ </div>
+ {newTokenType === TokenType.Project && (
+ <div className="input-large spacer-right display-flex-column">
+ <label htmlFor="token-select-project" className="text-bold">
+ {translate('users.tokens.project')}
+ </label>
+ <Select
+ id="token-select-project"
+ className="spacer-top it__project"
+ onChange={this.handleProjectChange}
+ options={projects}
+ placeholder={translate('users.tokens.select_project')}
+ value={projects.find(project => project.value === selectedProject.key)}
+ />
+ </div>
)}
</>
)}
-
- <SubmitButton className="it__generate-token" disabled={this.isSubmitButtonDisabled()}>
+ <div className="display-flex-column input-medium spacer-right ">
+ <label htmlFor="token-select-expiration" className="text-bold">
+ {translate('users.tokens.expires_in')}
+ </label>
+ <Select
+ id="token-select-expiration"
+ className="spacer-top"
+ isSearchable={false}
+ onChange={this.handleNewTokenExpirationChange}
+ options={EXPIRATION_OPTIONS}
+ value={EXPIRATION_OPTIONS.find(option => option.value === newTokenExpiration)}
+ />
+ </div>
+ <SubmitButton
+ className="it__generate-token"
+ style={{ marginTop: 'auto' }}
+ disabled={this.isSubmitButtonDisabled()}>
{translate('users.generate')}
</SubmitButton>
</form>
@@ -284,7 +356,7 @@ export class TokensForm extends React.PureComponent<Props, State> {
return (
<>
- <h3 className="spacer-bottom">{translate('users.generate_tokens')}</h3>
+ <h3 className="spacer-bottom">{translate('users.tokens.generate')}</h3>
{this.renderForm()}
{newToken && <TokensFormNewToken token={newToken} />}
diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/TokensForm-test.tsx b/server/sonar-web/src/main/js/apps/users/components/__tests__/TokensForm-test.tsx
deleted file mode 100644
index 7a51f8a17b2..00000000000
--- a/server/sonar-web/src/main/js/apps/users/components/__tests__/TokensForm-test.tsx
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import { shallow } from 'enzyme';
-import * as React from 'react';
-import { generateToken, getTokens } from '../../../../api/user-tokens';
-import { mockUserToken } from '../../../../helpers/mocks/token';
-import { mockCurrentUser } from '../../../../helpers/testMocks';
-import { change, submit, waitAndUpdate } from '../../../../helpers/testUtils';
-import { TokenType } from '../../../../types/token';
-import { TokensForm } from '../TokensForm';
-
-jest.mock('../../../../api/user-tokens', () => ({
- generateToken: jest.fn().mockResolvedValue({
- name: 'baz',
- createdAt: '2019-01-21T08:06:00+0100',
- login: 'luke',
- token: 'token_value'
- }),
- getTokens: jest.fn().mockResolvedValue([
- {
- name: 'foo',
- createdAt: '2019-01-15T15:06:33+0100',
- lastConnectionDate: '2019-01-18T15:06:33+0100'
- },
- { name: 'bar', createdAt: '2019-01-18T15:06:33+0100' }
- ])
-}));
-
-beforeEach(() => {
- (generateToken as jest.Mock).mockClear();
- (getTokens as jest.Mock).mockClear();
-});
-
-it('should render correctly', async () => {
- const wrapper = shallowRender();
- expect(wrapper).toMatchSnapshot();
- expect(getTokens).toHaveBeenCalledWith('luke');
-
- await waitAndUpdate(wrapper);
- expect(wrapper).toMatchSnapshot();
-});
-
-it('should create new tokens', async () => {
- const wrapper = shallowRender();
-
- await waitAndUpdate(wrapper);
- expect(wrapper.find('TokensFormItem')).toHaveLength(2);
- change(wrapper.find('input'), 'baz');
- submit(wrapper.find('form'));
-
- await waitAndUpdate(wrapper);
- expect(generateToken).toHaveBeenCalledWith({ name: 'baz', login: 'luke', type: TokenType.User });
- expect(wrapper.find('TokensFormItem')).toHaveLength(3);
-});
-
-it('should revoke tokens', async () => {
- const updateTokensCount = jest.fn();
- const wrapper = shallowRender({ updateTokensCount });
-
- await waitAndUpdate(wrapper);
- expect(wrapper.find('TokensFormItem')).toHaveLength(2);
- wrapper
- .instance()
- .handleRevokeToken(mockUserToken({ createdAt: '2019-01-15T15:06:33+0100', name: 'foo' }));
- expect(updateTokensCount).toHaveBeenCalledWith('luke', 1);
- expect(wrapper.find('TokensFormItem')).toHaveLength(1);
-});
-
-function shallowRender(props: Partial<TokensForm['props']> = {}) {
- return shallow<TokensForm>(
- <TokensForm
- deleteConfirmation="inline"
- login="luke"
- updateTokensCount={jest.fn()}
- displayTokenTypeInput={false}
- currentUser={mockCurrentUser()}
- {...props}
- />
- );
-}
diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensForm-test.tsx.snap
deleted file mode 100644
index 974d398732b..00000000000
--- a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensForm-test.tsx.snap
+++ /dev/null
@@ -1,192 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<Fragment>
- <h3
- className="spacer-bottom"
- >
- users.generate_tokens
- </h3>
- <form
- autoComplete="off"
- className="display-flex-center"
- onSubmit={[Function]}
- >
- <input
- className="input-large spacer-right it__token-name"
- maxLength={100}
- onChange={[Function]}
- placeholder="users.enter_token_name"
- required={true}
- type="text"
- value=""
- />
- <SubmitButton
- className="it__generate-token"
- disabled={true}
- >
- users.generate
- </SubmitButton>
- </form>
- <table
- className="data zebra big-spacer-top fixed"
- >
- <thead>
- <tr>
- <th>
- name
- </th>
- <th>
- my_account.token_type
- </th>
- <th>
- my_account.project_name
- </th>
- <th>
- my_account.tokens_last_usage
- </th>
- <th
- className="text-right"
- >
- created
- </th>
- <th
- className="text-right"
- >
- my_account.tokens.expiration
- </th>
- <th
- aria-label="actions"
- />
- </tr>
- </thead>
- <tbody>
- <DeferredSpinner
- customSpinner={
- <tr>
- <td>
- <i
- className="spinner"
- />
- </td>
- </tr>
- }
- loading={true}
- >
- <tr>
- <td
- className="note"
- colSpan={7}
- >
- users.no_tokens
- </td>
- </tr>
- </DeferredSpinner>
- </tbody>
- </table>
-</Fragment>
-`;
-
-exports[`should render correctly 2`] = `
-<Fragment>
- <h3
- className="spacer-bottom"
- >
- users.generate_tokens
- </h3>
- <form
- autoComplete="off"
- className="display-flex-center"
- onSubmit={[Function]}
- >
- <input
- className="input-large spacer-right it__token-name"
- maxLength={100}
- onChange={[Function]}
- placeholder="users.enter_token_name"
- required={true}
- type="text"
- value=""
- />
- <SubmitButton
- className="it__generate-token"
- disabled={true}
- >
- users.generate
- </SubmitButton>
- </form>
- <table
- className="data zebra big-spacer-top fixed"
- >
- <thead>
- <tr>
- <th>
- name
- </th>
- <th>
- my_account.token_type
- </th>
- <th>
- my_account.project_name
- </th>
- <th>
- my_account.tokens_last_usage
- </th>
- <th
- className="text-right"
- >
- created
- </th>
- <th
- className="text-right"
- >
- my_account.tokens.expiration
- </th>
- <th
- aria-label="actions"
- />
- </tr>
- </thead>
- <tbody>
- <DeferredSpinner
- customSpinner={
- <tr>
- <td>
- <i
- className="spinner"
- />
- </td>
- </tr>
- }
- loading={false}
- >
- <TokensFormItem
- deleteConfirmation="inline"
- key="foo"
- login="luke"
- onRevokeToken={[Function]}
- token={
- Object {
- "createdAt": "2019-01-15T15:06:33+0100",
- "lastConnectionDate": "2019-01-18T15:06:33+0100",
- "name": "foo",
- }
- }
- />
- <TokensFormItem
- deleteConfirmation="inline"
- key="bar"
- login="luke"
- onRevokeToken={[Function]}
- token={
- Object {
- "createdAt": "2019-01-18T15:06:33+0100",
- "name": "bar",
- }
- }
- />
- </DeferredSpinner>
- </tbody>
- </table>
-</Fragment>
-`;
diff --git a/server/sonar-web/src/main/js/helpers/dates.ts b/server/sonar-web/src/main/js/helpers/dates.ts
index 26a139a1c80..8f6abea0a78 100644
--- a/server/sonar-web/src/main/js/helpers/dates.ts
+++ b/server/sonar-web/src/main/js/helpers/dates.ts
@@ -44,3 +44,7 @@ export function toNotSoISOString(rawDate: ParsableDate): string {
export function isValidDate(date: Date): boolean {
return !isNaN(date.getTime());
}
+
+export function now() {
+ return new Date();
+}
diff --git a/server/sonar-web/src/main/js/types/token.ts b/server/sonar-web/src/main/js/types/token.ts
index acc95fe5754..9a9f3c23ec5 100644
--- a/server/sonar-web/src/main/js/types/token.ts
+++ b/server/sonar-web/src/main/js/types/token.ts
@@ -24,6 +24,13 @@ export enum TokenType {
User = 'USER_TOKEN'
}
+export enum TokenExpiration {
+ OneMonth = 30,
+ ThreeMonths = 90,
+ OneYear = 365,
+ NoExpiration = 0
+}
+
export interface UserToken {
name: string;
createdAt: string;
diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
index 2aad77b3586..cd1fd65f021 100644
--- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties
+++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
@@ -4107,10 +4107,18 @@ users.tokens.GLOBAL_ANALYSIS_TOKEN=Global Analysis Token
users.tokens.GLOBAL_ANALYSIS_TOKEN.short=Global
users.tokens.USER_TOKEN=User Token
users.tokens.USER_TOKEN.short=User
-users.generate_tokens=Generate Tokens
-users.enter_token_name=Enter Token Name
-users.select_token_type=Select Token Type
-users.select_token_project=Select Project
+users.tokens.generate=Generate Tokens
+users.tokens.name=Name
+users.tokens.enter_name=Enter Token Name
+users.tokens.type=Type
+users.tokens.select_type=Select Token Type
+users.tokens.project=Project
+users.tokens.select_project=Select Project
+users.tokens.expires_in=Expires in
+users.tokens.expiration.30=30 days
+users.tokens.expiration.90=90 days
+users.tokens.expiration.365=1 year
+users.tokens.expiration.0=No expiration
users.tokens.new_token_created=New token "{0}" has been created. Make sure you copy it now, you won't be able to see it again!
users.generate_new_token=Generate New Token
users.new_token=New token value