diff options
author | Stas Vilchik <stas.vilchik@sonarsource.com> | 2017-10-18 14:35:54 +0200 |
---|---|---|
committer | Stas Vilchik <stas.vilchik@sonarsource.com> | 2017-10-19 11:07:32 +0200 |
commit | e462c69f762dd0e775d7ca85e6c1e87e0551a08e (patch) | |
tree | 3cd5e38989816bddb945932191c5d81426f02c4e /server/sonar-web/src/main/js/apps/tutorials | |
parent | 1ff74014e9933a15fcad0f835c3206e746c54a50 (diff) | |
download | sonarqube-e462c69f762dd0e775d7ca85e6c1e87e0551a08e.tar.gz sonarqube-e462c69f762dd0e775d7ca85e6c1e87e0551a08e.zip |
pre-validate exising token during onboarding
Diffstat (limited to 'server/sonar-web/src/main/js/apps/tutorials')
5 files changed, 155 insertions, 48 deletions
diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/Onboarding.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/Onboarding.js index c5ec5148511..ad825f28a36 100644 --- a/server/sonar-web/src/main/js/apps/tutorials/onboarding/Onboarding.js +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/Onboarding.js @@ -190,6 +190,7 @@ export default class Onboarding extends React.PureComponent { )} <TokenStep + currentUser={this.props.currentUser} finished={this.state.token != null} onContinue={this.handleTokenDone} onOpen={this.handleTokenOpen} diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/TokenStep.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/TokenStep.js index abcdb98b84e..1bbaa9df1b4 100644 --- a/server/sonar-web/src/main/js/apps/tutorials/onboarding/TokenStep.js +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/TokenStep.js @@ -21,12 +21,14 @@ import React from 'react'; import classNames from 'classnames'; import Step from './Step'; +import { getTokens, generateToken, revokeToken } from '../../../api/user-tokens'; +import AlertErrorIcon from '../../../components/icons-components/AlertErrorIcon'; import CloseIcon from '../../../components/icons-components/CloseIcon'; -import { generateToken, revokeToken } from '../../../api/user-tokens'; import { translate } from '../../../helpers/l10n'; /*:: type Props = {| + currentUser: { login: string }, finished: boolean, open: boolean, onContinue: (token: string) => void, @@ -37,7 +39,8 @@ type Props = {| /*:: type State = { - existingToken:string, + canUseExisting: boolean, + existingToken: string, loading: boolean, selection: string, tokenName?: string, @@ -49,6 +52,7 @@ export default class TokenStep extends React.PureComponent { /*:: mounted: boolean; */ /*:: props: Props; */ state /*: State */ = { + canUseExisting: false, existingToken: '', loading: false, selection: 'generate' @@ -56,6 +60,14 @@ export default class TokenStep extends React.PureComponent { componentDidMount() { this.mounted = true; + getTokens(this.props.currentUser.login).then( + tokens => { + if (this.mounted) { + this.setState({ canUseExisting: tokens.length > 0 }); + } + }, + () => {} + ); } componentWillUnmount() { @@ -65,6 +77,15 @@ export default class TokenStep extends React.PureComponent { getToken = () => this.state.selection === 'generate' ? this.state.token : this.state.existingToken; + canContinue = () => { + const { existingToken, selection, token } = this.state; + const validExistingToken = existingToken.match(/^[a-z0-9]+$/) != null; + return ( + (selection === 'generate' && token != null) || + (selection === 'use-existing' && existingToken && validExistingToken) + ); + }; + handleTokenNameChange = (event /*: { target: HTMLInputElement } */) => { this.setState({ tokenName: event.target.value }); }; @@ -133,17 +154,21 @@ export default class TokenStep extends React.PureComponent { renderGenerateOption = () => ( <div> - <a - className="js-new link-base-color link-no-underline" - href="#" - onClick={this.handleGenerateClick}> - <i - className={classNames('icon-radio', 'spacer-right', { - 'is-checked': this.state.selection === 'generate' - })} - /> - {translate('onboading.token.generate_token')} - </a> + {this.state.canUseExisting ? ( + <a + className="js-new link-base-color link-no-underline" + href="#" + onClick={this.handleGenerateClick}> + <i + className={classNames('icon-radio', 'spacer-right', { + 'is-checked': this.state.selection === 'generate' + })} + /> + {translate('onboading.token.generate_token')} + </a> + ) : ( + translate('onboading.token.generate_token') + )} {this.state.selection === 'generate' && ( <div className="big-spacer-top"> <form onSubmit={this.handleTokenGenerate}> @@ -169,37 +194,48 @@ export default class TokenStep extends React.PureComponent { </div> ); - renderUseExistingOption = () => ( - <div className="big-spacer-top"> - <a - className="js-new link-base-color link-no-underline" - href="#" - onClick={this.handleUseExistingClick}> - <i - className={classNames('icon-radio', 'spacer-right', { - 'is-checked': this.state.selection === 'use-existing' - })} - /> - {translate('onboarding.token.use_existing_token')} - </a> - {this.state.selection === 'use-existing' && ( - <div className="big-spacer-top"> - <input - autoFocus={true} - className="input-large spacer-right text-middle" - onChange={this.handleExisingTokenChange} - placeholder={translate('onboarding.token.use_existing_token.placeholder')} - required={true} - type="text" - value={this.state.existingToken} + renderUseExistingOption = () => { + const { existingToken } = this.state; + const validInput = !existingToken || existingToken.match(/^[a-z0-9]+$/) != null; + + return ( + <div className="big-spacer-top"> + <a + className="js-new link-base-color link-no-underline" + href="#" + onClick={this.handleUseExistingClick}> + <i + className={classNames('icon-radio', 'spacer-right', { + 'is-checked': this.state.selection === 'use-existing' + })} /> - </div> - )} - </div> - ); + {translate('onboarding.token.use_existing_token')} + </a> + {this.state.selection === 'use-existing' && ( + <div className="big-spacer-top"> + <input + autoFocus={true} + className="input-large spacer-right text-middle" + onChange={this.handleExisingTokenChange} + placeholder={translate('onboarding.token.use_existing_token.placeholder')} + required={true} + type="text" + value={this.state.existingToken} + /> + {!validInput && ( + <span className="text-danger"> + <AlertErrorIcon className="little-spacer-right text-text-top" /> + {translate('onboarding.token.invalid_format')} + </span> + )} + </div> + )} + </div> + ); + }; renderForm = () => { - const { existingToken, loading, selection, token, tokenName } = this.state; + const { canUseExisting, loading, token, tokenName } = this.state; return ( <div className="boxed-group-inner"> @@ -221,14 +257,13 @@ export default class TokenStep extends React.PureComponent { ) : ( <div> {this.renderGenerateOption()} - {this.renderUseExistingOption()} + {canUseExisting && this.renderUseExistingOption()} </div> )} <div className="note big-spacer-top width-50">{translate('onboarding.token.text')}</div> - {((selection === 'generate' && token != null) || - (selection === 'use-existing' && existingToken)) && ( + {this.canContinue() && ( <div className="big-spacer-top"> <button className="js-continue" onClick={this.handleContinueClick}> {translate('continue')} diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/TokenStep-test.js b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/TokenStep-test.js index 2c2f07d5b75..200a9030052 100644 --- a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/TokenStep-test.js +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/TokenStep-test.js @@ -24,13 +24,17 @@ import TokenStep from '../TokenStep'; import { change, click, doAsync, submit } from '../../../../helpers/testUtils'; jest.mock('../../../../api/user-tokens', () => ({ + getTokens: () => Promise.resolve([{ name: 'foo' }]), generateToken: () => Promise.resolve({ token: 'abcd1234' }), revokeToken: () => Promise.resolve() })); -it('generates token', () => { +const currentUser = { login: 'user' }; + +it('generates token', async () => { const wrapper = mount( <TokenStep + currentUser={currentUser} finished={false} open={true} onContinue={jest.fn()} @@ -38,6 +42,7 @@ it('generates token', () => { stepNumber={1} /> ); + await new Promise(setImmediate); expect(wrapper).toMatchSnapshot(); change(wrapper.find('input'), 'my token'); submit(wrapper.find('form')); @@ -45,9 +50,10 @@ it('generates token', () => { return doAsync(() => expect(wrapper).toMatchSnapshot()); }); -it('revokes token', () => { +it('revokes token', async () => { const wrapper = mount( <TokenStep + currentUser={currentUser} finished={false} open={true} onContinue={jest.fn()} @@ -55,6 +61,7 @@ it('revokes token', () => { stepNumber={1} /> ); + await new Promise(setImmediate); wrapper.setState({ token: 'abcd1234', tokenName: 'my token' }); expect(wrapper).toMatchSnapshot(); submit(wrapper.find('form')); @@ -62,10 +69,11 @@ it('revokes token', () => { return doAsync(() => expect(wrapper).toMatchSnapshot()); }); -it('continues', () => { +it('continues', async () => { const onContinue = jest.fn(); const wrapper = mount( <TokenStep + currentUser={currentUser} finished={false} open={true} onContinue={onContinue} @@ -73,15 +81,17 @@ it('continues', () => { stepNumber={1} /> ); + await new Promise(setImmediate); wrapper.setState({ token: 'abcd1234', tokenName: 'my token' }); click(wrapper.find('.js-continue')); expect(onContinue).toBeCalledWith('abcd1234'); }); -it('uses existing token', () => { +it('uses existing token', async () => { const onContinue = jest.fn(); const wrapper = mount( <TokenStep + currentUser={currentUser} finished={false} open={true} onContinue={onContinue} @@ -89,6 +99,7 @@ it('uses existing token', () => { stepNumber={1} /> ); + await new Promise(setImmediate); wrapper.setState({ existingToken: 'abcd1234', selection: 'use-existing' }); click(wrapper.find('.js-continue')); expect(onContinue).toBeCalledWith('abcd1234'); diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/Onboarding-test.js.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/Onboarding-test.js.snap index 3fed8ff9708..55409e86250 100644 --- a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/Onboarding-test.js.snap +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/Onboarding-test.js.snap @@ -43,6 +43,12 @@ exports[`guides for on-premise 1`] = ` </div> </header> <TokenStep + currentUser={ + Object { + "isLoggedIn": true, + "login": "admin", + } + } finished={false} onContinue={[Function]} onOpen={[Function]} @@ -103,6 +109,12 @@ exports[`guides for on-premise 2`] = ` </div> </header> <TokenStep + currentUser={ + Object { + "isLoggedIn": true, + "login": "admin", + } + } finished={true} onContinue={[Function]} onOpen={[Function]} @@ -177,6 +189,12 @@ exports[`guides for sonarcloud 1`] = ` stepNumber={1} /> <TokenStep + currentUser={ + Object { + "isLoggedIn": true, + "login": "admin", + } + } finished={false} onContinue={[Function]} onOpen={[Function]} @@ -250,6 +268,12 @@ exports[`guides for sonarcloud 2`] = ` stepNumber={1} /> <TokenStep + currentUser={ + Object { + "isLoggedIn": true, + "login": "admin", + } + } finished={false} onContinue={[Function]} onOpen={[Function]} @@ -324,6 +348,12 @@ exports[`guides for sonarcloud 3`] = ` stepNumber={1} /> <TokenStep + currentUser={ + Object { + "isLoggedIn": true, + "login": "admin", + } + } finished={true} onContinue={[Function]} onOpen={[Function]} diff --git a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/TokenStep-test.js.snap b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/TokenStep-test.js.snap index 38b83ffa20a..5afe9b488f1 100644 --- a/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/TokenStep-test.js.snap +++ b/server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/TokenStep-test.js.snap @@ -2,6 +2,11 @@ exports[`generates token 1`] = ` <TokenStep + currentUser={ + Object { + "login": "user", + } + } finished={false} onContinue={[Function]} onOpen={[Function]} @@ -99,6 +104,11 @@ exports[`generates token 1`] = ` exports[`generates token 2`] = ` <TokenStep + currentUser={ + Object { + "login": "user", + } + } finished={false} onContinue={[Function]} onOpen={[Function]} @@ -193,6 +203,11 @@ exports[`generates token 2`] = ` exports[`generates token 3`] = ` <TokenStep + currentUser={ + Object { + "login": "user", + } + } finished={false} onContinue={[Function]} onOpen={[Function]} @@ -285,6 +300,11 @@ exports[`generates token 3`] = ` exports[`revokes token 1`] = ` <TokenStep + currentUser={ + Object { + "login": "user", + } + } finished={false} onContinue={[Function]} onOpen={[Function]} @@ -377,6 +397,11 @@ exports[`revokes token 1`] = ` exports[`revokes token 2`] = ` <TokenStep + currentUser={ + Object { + "login": "user", + } + } finished={false} onContinue={[Function]} onOpen={[Function]} @@ -451,6 +476,11 @@ exports[`revokes token 2`] = ` exports[`revokes token 3`] = ` <TokenStep + currentUser={ + Object { + "login": "user", + } + } finished={false} onContinue={[Function]} onOpen={[Function]} |