diff options
author | Guillaume Peoc'h <guillaume.peoch@sonarsource.com> | 2022-04-14 17:10:20 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2022-04-29 20:03:19 +0000 |
commit | ac2259ccb777e0f143d828debc101d34444ad35f (patch) | |
tree | e95ef415420d5cf20ebf4c21c250bc9bf7d5d931 | |
parent | 0e8b19279b5c24eb73bcfbc225264ead31796fc7 (diff) | |
download | sonarqube-ac2259ccb777e0f143d828debc101d34444ad35f.tar.gz sonarqube-ac2259ccb777e0f143d828debc101d34444ad35f.zip |
SONAR-16263 Token creation
20 files changed, 312 insertions, 113 deletions
diff --git a/server/sonar-web/src/main/js/api/components.ts b/server/sonar-web/src/main/js/api/components.ts index eebd23b059f..ea9c6f2621e 100644 --- a/server/sonar-web/src/main/js/api/components.ts +++ b/server/sonar-web/src/main/js/api/components.ts @@ -299,3 +299,12 @@ export function getTests( ): Promise<any> { return getJSON('/api/tests/list', data).then(r => r.tests); } + +interface ProjectResponse { + key: string; + name: string; +} + +export function getScannableProjects(): Promise<{ projects: ProjectResponse[] }> { + return getJSON('/api/projects/search_my_scannable_projects').catch(throwGlobalError); +} diff --git a/server/sonar-web/src/main/js/api/mocks/UserTokensMock.ts b/server/sonar-web/src/main/js/api/mocks/UserTokensMock.ts index 9348f46a5ca..18f26798257 100644 --- a/server/sonar-web/src/main/js/api/mocks/UserTokensMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/UserTokensMock.ts @@ -19,7 +19,7 @@ */ import { cloneDeep } from 'lodash'; -import { NewUserToken, UserToken } from '../../types/types'; +import { NewUserToken, UserToken } from '../../types/token'; import { generateToken, getTokens, revokeToken } from '../user-tokens'; const RANDOM_RADIX = 36; 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 53daaf04b4b..1cb261c2a8e 100644 --- a/server/sonar-web/src/main/js/api/user-tokens.ts +++ b/server/sonar-web/src/main/js/api/user-tokens.ts @@ -19,14 +19,19 @@ */ import { throwGlobalError } from '../helpers/error'; import { getJSON, post, postJSON } from '../helpers/request'; -import { NewUserToken, UserToken } from '../types/types'; +import { NewUserToken, UserToken } from '../types/token'; /** List tokens for given user login */ export function getTokens(login: string): Promise<UserToken[]> { return getJSON('/api/user_tokens/search', { login }).then(r => r.userTokens, throwGlobalError); } -export function generateToken(data: { name: string; login?: string }): Promise<NewUserToken> { +export function generateToken(data: { + name: string; + projectKey?: string; + type?: string; + login?: 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 3190d691de7..87511d9efc4 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 @@ -20,12 +20,15 @@ import { screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { UserEvent } from '@testing-library/user-event/dist/types/setup'; +import selectEvent from 'react-select-event'; import { getMyProjects } from '../../../api/components'; import NotificationsMock from '../../../api/mocks/NotificationsMock'; import UserTokensMock from '../../../api/mocks/UserTokensMock'; import getHistory from '../../../helpers/getHistory'; import { mockCurrentUser, mockLoggedInUser } from '../../../helpers/testMocks'; import { renderApp } from '../../../helpers/testReactTestingUtils'; +import { Permissions } from '../../../types/permissions'; +import { TokenType } from '../../../types/token'; import { CurrentUser } from '../../../types/users'; import routes from '../routes'; @@ -96,6 +99,18 @@ jest.mock('../../../api/components', () => ({ ] } ] + }), + getScannableProjects: jest.fn().mockResolvedValue({ + projects: [ + { + key: 'project-key-1', + name: 'Project Name 1' + }, + { + key: 'project-key-2', + name: 'Project Name 2' + } + ] }) })); @@ -192,47 +207,77 @@ describe('security page', () => { const securityPagePath = 'account/security'; - it('should allow token creation/revoking and display existing tokens', async () => { - const user = userEvent.setup(); - - renderAccountApp(mockLoggedInUser(), securityPagePath); - - expect(await screen.findByText('users.tokens')).toBeInTheDocument(); - expect(screen.getAllByRole('row')).toHaveLength(3); // 2 tokens + header - - // Add a token - const newTokenName = 'importantToken'; - const input = screen.getByRole('textbox'); - const generateButton = screen.getByRole('button', { name: 'users.generate' }); - expect(input).toBeInTheDocument(); - await user.click(input); - await user.keyboard(newTokenName); + it.each([ + ['user', TokenType.User], + ['global', TokenType.Global], + ['project analysis', TokenType.Project] + ])( + 'should allow %s token creation/revocation and display existing tokens', + async (_, tokenTypeOption) => { + const user = userEvent.setup(); + + renderAccountApp( + mockLoggedInUser({ permissions: { global: [Permissions.Scan] } }), + securityPagePath + ); + + expect(await screen.findByText('users.tokens')).toBeInTheDocument(); + expect(screen.getAllByRole('row')).toHaveLength(3); // 2 tokens + header + + // Add the token + const newTokenName = 'importantToken'; + const input = screen.getByPlaceholderText('users.enter_token_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.${tokenTypeOption}`; + + if (tokenTypeOption === TokenType.Project) { + await selectEvent.select(screen.getAllByRole('textbox')[1], [tokenTypeLabel]); + expect(generateButton).toBeDisabled(); + expect(screen.getAllByRole('textbox')).toHaveLength(3); + await selectEvent.select(screen.getAllByRole('textbox')[2], ['Project Name 1']); + expect(generateButton).not.toBeDisabled(); + } else { + await selectEvent.select(screen.getAllByRole('textbox')[1], [tokenTypeLabel]); + expect(generateButton).not.toBeDisabled(); + } - expect(generateButton).not.toBeDisabled(); - await user.click(generateButton); + await user.click(generateButton); - expect( - await screen.findByText(`users.tokens.new_token_created.${newTokenName}`) - ).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'copy' })).toBeInTheDocument(); + expect( + await screen.findByText(`users.tokens.new_token_created.${newTokenName}`) + ).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'copy' })).toBeInTheDocument(); - const lastTokenCreated = tokenMock.getTokens().pop(); - expect(lastTokenCreated).toBeDefined(); - expect(screen.getByLabelText('users.new_token').textContent).toBe(lastTokenCreated!.token); + const lastTokenCreated = tokenMock.getTokens().pop(); + expect(lastTokenCreated).toBeDefined(); + expect(screen.getByLabelText('users.new_token').textContent).toBe(lastTokenCreated!.token); - expect(screen.getAllByRole('row')).toHaveLength(4); // 3 tokens + header + expect(screen.getAllByRole('row')).toHaveLength(4); // 3 tokens + header - // Revoke a token - const revokeButtons = screen.getAllByRole('button', { name: 'users.tokens.revoke_token' }); - expect(revokeButtons).toHaveLength(3); - await user.click(revokeButtons[1]); + // Revoke the token + const row = screen.getAllByRole('row', { + name: (n: string) => n.includes(newTokenName) + }); + const revokeButtons = within(row[0]).getByRole('button', { + name: 'users.tokens.revoke_token' + }); + await user.click(revokeButtons); - expect(screen.getByRole('heading', { name: 'users.tokens.revoke_token' })).toBeInTheDocument(); + expect( + screen.getByRole('heading', { name: 'users.tokens.revoke_token' }) + ).toBeInTheDocument(); - await user.click(screen.getByText('users.tokens.revoke_token', { selector: 'button' })); + await user.click(screen.getByText('users.tokens.revoke_token', { selector: 'button' })); - expect(screen.getAllByRole('row')).toHaveLength(3); // 2 tokens + header - }); + expect(screen.getAllByRole('row')).toHaveLength(3); // 2 tokens + header + } + ); it('should allow local users to change password', async () => { const user = userEvent.setup(); diff --git a/server/sonar-web/src/main/js/apps/account/account.css b/server/sonar-web/src/main/js/apps/account/account.css index 322907768fe..f05b97da368 100644 --- a/server/sonar-web/src/main/js/apps/account/account.css +++ b/server/sonar-web/src/main/js/apps/account/account.css @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ .account-container { - width: 600px; + width: 700px; margin-left: auto; margin-right: auto; } diff --git a/server/sonar-web/src/main/js/apps/account/security/Tokens.tsx b/server/sonar-web/src/main/js/apps/account/security/Tokens.tsx index 44c67d9f9a0..ab586526275 100644 --- a/server/sonar-web/src/main/js/apps/account/security/Tokens.tsx +++ b/server/sonar-web/src/main/js/apps/account/security/Tokens.tsx @@ -35,7 +35,7 @@ export default function Tokens({ login }: Props) { <InstanceMessage message={translate('my_account.tokens_description')} /> </div> - <TokensForm deleteConfirmation="modal" login={login} /> + <TokensForm deleteConfirmation="modal" login={login} displayTokenTypeInput={true} /> </div> </div> ); 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 aa086e8f470..efc7a785bd2 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 @@ -18,11 +18,17 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import { getScannableProjects } from '../../../api/components'; import { generateToken, getTokens } from '../../../api/user-tokens'; +import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext'; import { SubmitButton } from '../../../components/controls/buttons'; +import Select, { BasicSelectOption } from '../../../components/controls/Select'; import DeferredSpinner from '../../../components/ui/DeferredSpinner'; import { translate } from '../../../helpers/l10n'; -import { UserToken } from '../../../types/types'; +import { hasGlobalPermission } from '../../../helpers/users'; +import { Permissions } from '../../../types/permissions'; +import { TokenType, UserToken } from '../../../types/token'; +import { CurrentUser } from '../../../types/users'; import TokensFormItem, { TokenDeleteConfirmation } from './TokensFormItem'; import TokensFormNewToken from './TokensFormNewToken'; @@ -30,6 +36,8 @@ interface Props { deleteConfirmation: TokenDeleteConfirmation; login: string; updateTokensCount?: (login: string, tokensCount: number) => void; + displayTokenTypeInput: boolean; + currentUser: CurrentUser; } interface State { @@ -37,16 +45,22 @@ interface State { loading: boolean; newToken?: { name: string; token: string }; newTokenName: string; + newTokenType?: TokenType; tokens: UserToken[]; + projects: BasicSelectOption[]; + selectedProjectkey?: string; } -export default class TokensForm extends React.PureComponent<Props, State> { +export class TokensForm extends React.PureComponent<Props, State> { mounted = false; state: State = { generating: false, loading: true, newTokenName: '', - tokens: [] + newTokenType: this.props.displayTokenTypeInput ? undefined : TokenType.User, + selectedProjectkey: '', + tokens: [], + projects: [] }; componentDidMount() { @@ -74,34 +88,51 @@ export default class TokensForm extends React.PureComponent<Props, State> { ); }; + fetchProjects = async () => { + const { projects: projectArray } = await getScannableProjects(); + const projects = projectArray.map(project => ({ label: project.name, value: project.key })); + this.setState({ + projects + }); + }; + updateTokensCount = () => { if (this.props.updateTokensCount) { this.props.updateTokensCount(this.props.login, this.state.tokens.length); } }; - handleGenerateToken = (evt: React.SyntheticEvent<HTMLFormElement>) => { - evt.preventDefault(); - if (this.state.newTokenName.length > 0) { - this.setState({ generating: true }); - generateToken({ name: this.state.newTokenName, login: this.props.login }).then( - newToken => { - if (this.mounted) { - this.setState(state => { - const tokens = [ - ...state.tokens, - { name: newToken.name, createdAt: newToken.createdAt } - ]; - return { generating: false, newToken, newTokenName: '', tokens }; - }, this.updateTokensCount); - } - }, - () => { - if (this.mounted) { - this.setState({ generating: false }); - } - } - ); + handleGenerateToken = async (event: React.SyntheticEvent<HTMLFormElement>) => { + event.preventDefault(); + const { login } = this.props; + const { newTokenName, newTokenType, selectedProjectkey } = this.state; + this.setState({ generating: true }); + + try { + const newToken = await generateToken({ + name: newTokenName, + login, + type: newTokenType, + ...(newTokenType === TokenType.Project && { projectKey: selectedProjectkey }) + }); + + if (this.mounted) { + this.setState(state => { + const tokens = [...state.tokens, { name: newToken.name, createdAt: newToken.createdAt }]; + return { + generating: false, + newToken, + newTokenName: '', + selectedProjectkey: '', + newTokenType: undefined, + tokens + }; + }, this.updateTokensCount); + } + } catch (e) { + if (this.mounted) { + this.setState({ generating: false }); + } } }; @@ -114,8 +145,93 @@ export default class TokensForm extends React.PureComponent<Props, State> { ); }; - handleNewTokenChange = (evt: React.SyntheticEvent<HTMLInputElement>) => + isSubmitButtonDisabled = () => { + const { displayTokenTypeInput } = this.props; + const { generating, newTokenName, newTokenType, selectedProjectkey } = this.state; + + if (!displayTokenTypeInput) { + return generating || newTokenName.length <= 0; + } + + if (generating || newTokenName.length <= 0) { + return true; + } + if (newTokenType === TokenType.Project) { + return !selectedProjectkey; + } + + return !newTokenType; + }; + + handleNewTokenChange = (evt: React.SyntheticEvent<HTMLInputElement>) => { this.setState({ newTokenName: evt.currentTarget.value }); + }; + + handleNewTokenTypeChange = ({ value }: { value: TokenType }) => { + if (value === TokenType.Project && this.state.projects.length === 0) { + this.fetchProjects(); + } + this.setState({ newTokenType: value }); + }; + + handleProjectChange = ({ value }: { value: string }) => { + this.setState({ selectedProjectkey: value }); + }; + + renderForm() { + const { newTokenName, newTokenType, projects, selectedProjectkey } = this.state; + const { displayTokenTypeInput, currentUser } = this.props; + + const tokenTypeOptions = [ + { label: translate('users.tokens', TokenType.Project), value: TokenType.Project }, + { label: translate('users.tokens', TokenType.User), value: TokenType.User } + ]; + if (hasGlobalPermission(currentUser, Permissions.Scan)) { + tokenTypeOptions.push({ + label: translate('users.tokens', TokenType.Global), + value: TokenType.Global + }); + } + + 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} + /> + {displayTokenTypeInput && ( + <> + <Select + className="input-medium 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 && ( + <Select + className="input-medium spacer-right it__project" + onChange={this.handleProjectChange} + options={projects} + placeholder={translate('users.select_token_project')} + value={projects.find(project => project.value === selectedProjectkey)} + /> + )} + </> + )} + + <SubmitButton className="it__generate-token" disabled={this.isSubmitButtonDisabled()}> + {translate('users.generate')} + </SubmitButton> + </form> + ); + } renderItems() { const { tokens } = this.state; @@ -140,7 +256,7 @@ export default class TokensForm extends React.PureComponent<Props, State> { } render() { - const { generating, loading, newToken, newTokenName, tokens } = this.state; + const { loading, newToken, tokens } = this.state; const customSpinner = ( <tr> <td> @@ -152,27 +268,7 @@ export default class TokensForm extends React.PureComponent<Props, State> { return ( <> <h3 className="spacer-bottom">{translate('users.generate_tokens')}</h3> - <form - autoComplete="off" - className="display-flex-center" - id="generate-token-form" - onSubmit={this.handleGenerateToken}> - <input - className="input-large spacer-right" - maxLength={100} - onChange={this.handleNewTokenChange} - placeholder={translate('users.enter_token_name')} - required={true} - type="text" - value={newTokenName} - /> - <SubmitButton - className="js-generate-token" - disabled={generating || newTokenName.length <= 0}> - {translate('users.generate')} - </SubmitButton> - </form> - + {this.renderForm()} {newToken && <TokensFormNewToken token={newToken} />} <table className="data zebra big-spacer-top"> @@ -194,3 +290,5 @@ export default class TokensForm extends React.PureComponent<Props, State> { ); } } + +export default withCurrentUserContext(TokensForm); diff --git a/server/sonar-web/src/main/js/apps/users/components/TokensFormItem.tsx b/server/sonar-web/src/main/js/apps/users/components/TokensFormItem.tsx index 3bd08ee79d3..54e448af15a 100644 --- a/server/sonar-web/src/main/js/apps/users/components/TokensFormItem.tsx +++ b/server/sonar-web/src/main/js/apps/users/components/TokensFormItem.tsx @@ -28,7 +28,7 @@ import DateFromNow from '../../../components/intl/DateFromNow'; import DeferredSpinner from '../../../components/ui/DeferredSpinner'; import { translate } from '../../../helpers/l10n'; import { limitComponentName } from '../../../helpers/path'; -import { UserToken } from '../../../types/types'; +import { UserToken } from '../../../types/token'; export type TokenDeleteConfirmation = 'inline' | 'modal'; diff --git a/server/sonar-web/src/main/js/apps/users/components/TokensFormModal.tsx b/server/sonar-web/src/main/js/apps/users/components/TokensFormModal.tsx index 85981cf315e..adffa467372 100644 --- a/server/sonar-web/src/main/js/apps/users/components/TokensFormModal.tsx +++ b/server/sonar-web/src/main/js/apps/users/components/TokensFormModal.tsx @@ -48,6 +48,7 @@ export default function TokensFormModal(props: Props) { deleteConfirmation="inline" login={props.user.login} updateTokensCount={props.updateTokensCount} + displayTokenTypeInput={false} /> </div> <footer className="modal-foot"> 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 index ca6877bdee5..c1a47007695 100644 --- 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 @@ -20,8 +20,10 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { generateToken, getTokens } from '../../../../api/user-tokens'; +import { mockCurrentUser } from '../../../../helpers/testMocks'; import { change, submit, waitAndUpdate } from '../../../../helpers/testUtils'; -import TokensForm from '../TokensForm'; +import { TokenType } from '../../../../types/token'; +import { TokensForm } from '../TokensForm'; jest.mock('../../../../api/user-tokens', () => ({ generateToken: jest.fn().mockResolvedValue({ @@ -63,7 +65,7 @@ it('should create new tokens', async () => { submit(wrapper.find('form')); await waitAndUpdate(wrapper); - expect(generateToken).toHaveBeenCalledWith({ name: 'baz', login: 'luke' }); + expect(generateToken).toHaveBeenCalledWith({ name: 'baz', login: 'luke', type: TokenType.User }); expect(wrapper.find('TokensFormItem')).toHaveLength(3); }); @@ -80,6 +82,13 @@ it('should revoke tokens', async () => { function shallowRender(props: Partial<TokensForm['props']> = {}) { return shallow<TokensForm>( - <TokensForm deleteConfirmation="inline" login="luke" updateTokensCount={jest.fn()} {...props} /> + <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__/TokensFormItem-test.tsx b/server/sonar-web/src/main/js/apps/users/components/__tests__/TokensFormItem-test.tsx index 0719dcd129c..5bbd567c3ce 100644 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/TokensFormItem-test.tsx +++ b/server/sonar-web/src/main/js/apps/users/components/__tests__/TokensFormItem-test.tsx @@ -21,7 +21,7 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { revokeToken } from '../../../../api/user-tokens'; import { click, waitAndUpdate } from '../../../../helpers/testUtils'; -import { UserToken } from '../../../../types/types'; +import { UserToken } from '../../../../types/token'; import TokensFormItem from '../TokensFormItem'; jest.mock('../../../../components/intl/DateFormatter'); 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 index 4b2092b196c..e7f3202ed19 100644 --- 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 @@ -10,11 +10,10 @@ exports[`should render correctly 1`] = ` <form autoComplete="off" className="display-flex-center" - id="generate-token-form" onSubmit={[Function]} > <input - className="input-large spacer-right" + className="input-large spacer-right it__token-name" maxLength={100} onChange={[Function]} placeholder="users.enter_token_name" @@ -23,7 +22,7 @@ exports[`should render correctly 1`] = ` value="" /> <SubmitButton - className="js-generate-token" + className="it__generate-token" disabled={true} > users.generate @@ -85,11 +84,10 @@ exports[`should render correctly 2`] = ` <form autoComplete="off" className="display-flex-center" - id="generate-token-form" onSubmit={[Function]} > <input - className="input-large spacer-right" + className="input-large spacer-right it__token-name" maxLength={100} onChange={[Function]} placeholder="users.enter_token_name" @@ -98,7 +96,7 @@ exports[`should render correctly 2`] = ` value="" /> <SubmitButton - className="js-generate-token" + className="it__generate-token" disabled={true} > users.generate diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensFormModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensFormModal-test.tsx.snap index 436ea23f061..6738ba5964c 100644 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensFormModal-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/TokensFormModal-test.tsx.snap @@ -25,8 +25,9 @@ exports[`should render correctly 1`] = ` <div className="modal-body modal-container" > - <TokensForm + <withCurrentUserContext(TokensForm) deleteConfirmation="inline" + displayTokenTypeInput={false} login="john.doe" updateTokensCount={[MockFunction]} /> diff --git a/server/sonar-web/src/main/js/components/tutorials/__tests__/utils-test.ts b/server/sonar-web/src/main/js/components/tutorials/__tests__/utils-test.ts index db06e7d6989..35382d5004b 100644 --- a/server/sonar-web/src/main/js/components/tutorials/__tests__/utils-test.ts +++ b/server/sonar-web/src/main/js/components/tutorials/__tests__/utils-test.ts @@ -22,7 +22,7 @@ import { mockProjectBitbucketCloudBindingResponse, mockProjectGithubBindingResponse } from '../../../helpers/mocks/alm-settings'; -import { UserToken } from '../../../types/types'; +import { UserToken } from '../../../types/token'; import { buildBitbucketCloudLink, buildGithubLink, getUniqueTokenName } from '../utils'; describe('getUniqueTokenName', () => { diff --git a/server/sonar-web/src/main/js/components/tutorials/manual/TokenStep.tsx b/server/sonar-web/src/main/js/components/tutorials/manual/TokenStep.tsx index df9e3b63869..ae7bd3fd968 100644 --- a/server/sonar-web/src/main/js/components/tutorials/manual/TokenStep.tsx +++ b/server/sonar-web/src/main/js/components/tutorials/manual/TokenStep.tsx @@ -25,7 +25,7 @@ import { Button, DeleteButton, SubmitButton } from '../../../components/controls import Radio from '../../../components/controls/Radio'; import AlertSuccessIcon from '../../../components/icons/AlertSuccessIcon'; import { translate } from '../../../helpers/l10n'; -import { UserToken } from '../../../types/types'; +import { UserToken } from '../../../types/token'; import { LoggedInUser } from '../../../types/users'; import AlertErrorIcon from '../../icons/AlertErrorIcon'; import Step from '../components/Step'; diff --git a/server/sonar-web/src/main/js/components/tutorials/utils.ts b/server/sonar-web/src/main/js/components/tutorials/utils.ts index 9ab0680b696..6e040c3f09a 100644 --- a/server/sonar-web/src/main/js/components/tutorials/utils.ts +++ b/server/sonar-web/src/main/js/components/tutorials/utils.ts @@ -19,7 +19,7 @@ */ import { convertGithubApiUrlToLink, stripTrailingSlash } from '../../helpers/urls'; import { AlmSettingsInstance, ProjectAlmBindingResponse } from '../../types/alm-settings'; -import { UserToken } from '../../types/types'; +import { UserToken } from '../../types/token'; export function quote(os: string): (s: string) => string { return os === 'win' ? (s: string) => `"${s}"` : (s: string) => s; diff --git a/server/sonar-web/src/main/js/types/permissions.ts b/server/sonar-web/src/main/js/types/permissions.ts index 9145adf37f9..f87015df8c1 100644 --- a/server/sonar-web/src/main/js/types/permissions.ts +++ b/server/sonar-web/src/main/js/types/permissions.ts @@ -21,5 +21,6 @@ export enum Permissions { Admin = 'admin', ProjectCreation = 'provisioning', ApplicationCreation = 'applicationcreator', - QualityGateAdmin = 'gateadmin' + QualityGateAdmin = 'gateadmin', + Scan = 'scan' } diff --git a/server/sonar-web/src/main/js/types/token.ts b/server/sonar-web/src/main/js/types/token.ts new file mode 100644 index 00000000000..50681e01da4 --- /dev/null +++ b/server/sonar-web/src/main/js/types/token.ts @@ -0,0 +1,38 @@ +/* + * 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. + */ + +export enum TokenType { + Project = 'PROJECT_ANALYSIS_TOKEN', + Global = 'GLOBAL_ANALYSIS_TOKEN', + User = 'USER_TOKEN' +} + +export interface UserToken { + name: string; + createdAt: string; + lastConnectionDate?: string; + type?: TokenType; + project?: { name: string; key: string }; +} + +export interface NewUserToken extends UserToken { + login: string; + token: string; +} diff --git a/server/sonar-web/src/main/js/types/types.ts b/server/sonar-web/src/main/js/types/types.ts index ae12b847d84..a376854de4a 100644 --- a/server/sonar-web/src/main/js/types/types.ts +++ b/server/sonar-web/src/main/js/types/types.ts @@ -767,17 +767,6 @@ export interface UserSelected extends UserActive { selected: boolean; } -export interface UserToken { - name: string; - createdAt: string; - lastConnectionDate?: string; -} - -export interface NewUserToken extends UserToken { - login: string; - token: string; -} - export type Visibility = 'public' | 'private'; export interface Webhook { 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 c699a344f70..13a5cb029c1 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -4077,8 +4077,13 @@ users.tokens.revoke=Revoke users.tokens.revoke_token=Revoke token users.no_tokens=No tokens users.generate=Generate +users.tokens.PROJECT_ANALYSIS_TOKEN=Project Analysis Token +users.tokens.GLOBAL_ANALYSIS_TOKEN=Global Analysis Token +users.tokens.USER_TOKEN=User Token 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.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 |