diff options
author | Guillaume Peoc'h <guillaume.peoch@sonarsource.com> | 2022-06-29 17:31:44 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2022-07-05 20:02:54 +0000 |
commit | bb411d21619c2aeadcd31df5ab1f68e8d819ac09 (patch) | |
tree | f2194908d9ffea7e914dde2b16a7fde09a2392e4 | |
parent | a2d4cdc405dd84cfaa76ec29b6bad6e12358cdfd (diff) | |
download | sonarqube-bb411d21619c2aeadcd31df5ab1f68e8d819ac09.tar.gz sonarqube-bb411d21619c2aeadcd31df5ab1f68e8d819ac09.zip |
SONAR-16565 Allow users to select the token expiration interval
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 |