diff options
6 files changed, 278 insertions, 81 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 a9065a16b0a..d0e4b41b234 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 @@ -172,7 +172,7 @@ export default class Onboarding extends React.PureComponent { </div> <div className="page-description"> {translateWithParameters( - 'onboarding.header.description_x', + 'onboarding.header.description', organizationsEnabled ? 3 : 2 )} </div> 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 80ed22c805d..abcdb98b84e 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 @@ -19,6 +19,7 @@ */ // @flow import React from 'react'; +import classNames from 'classnames'; import Step from './Step'; import CloseIcon from '../../../components/icons-components/CloseIcon'; import { generateToken, revokeToken } from '../../../api/user-tokens'; @@ -36,7 +37,9 @@ type Props = {| /*:: type State = { + existingToken:string, loading: boolean, + selection: string, tokenName?: string, token?: string }; @@ -46,7 +49,9 @@ export default class TokenStep extends React.PureComponent { /*:: mounted: boolean; */ /*:: props: Props; */ state /*: State */ = { - loading: false + existingToken: '', + loading: false, + selection: 'generate' }; componentDidMount() { @@ -57,6 +62,9 @@ export default class TokenStep extends React.PureComponent { this.mounted = false; } + getToken = () => + this.state.selection === 'generate' ? this.state.token : this.state.existingToken; + handleTokenNameChange = (event /*: { target: HTMLInputElement } */) => { this.setState({ tokenName: event.target.value }); }; @@ -103,13 +111,95 @@ export default class TokenStep extends React.PureComponent { handleContinueClick = (event /*: Event */) => { event.preventDefault(); - if (this.state.token) { - this.props.onContinue(this.state.token); + const token = this.getToken(); + if (token) { + this.props.onContinue(token); } }; + handleGenerateClick = (event /*: Event */) => { + event.preventDefault(); + this.setState({ selection: 'generate' }); + }; + + handleUseExistingClick = (event /*: Event */) => { + event.preventDefault(); + this.setState({ selection: 'use-existing' }); + }; + + handleExisingTokenChange = (event /*: { currentTarget: HTMLInputElement } */) => { + this.setState({ existingToken: event.currentTarget.value }); + }; + + 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.selection === 'generate' && ( + <div className="big-spacer-top"> + <form onSubmit={this.handleTokenGenerate}> + <input + autoFocus={true} + className="input-large spacer-right text-middle" + onChange={this.handleTokenNameChange} + placeholder={translate('onboading.token.generate_token.placeholder')} + required={true} + type="text" + value={this.state.tokenName || ''} + /> + {this.state.loading ? ( + <i className="spinner text-middle" /> + ) : ( + <button className="text-middle" disabled={!this.state.tokenName}> + {translate('onboarding.token.generate')} + </button> + )} + </form> + </div> + )} + </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} + /> + </div> + )} + </div> + ); + renderForm = () => { - const { loading, token, tokenName } = this.state; + const { existingToken, loading, selection, token, tokenName } = this.state; return ( <div className="boxed-group-inner"> @@ -129,27 +219,16 @@ export default class TokenStep extends React.PureComponent { )} </form> ) : ( - <form onSubmit={this.handleTokenGenerate}> - <input - autoFocus={true} - className="input-large spacer-right text-middle" - onChange={this.handleTokenNameChange} - placeholder={translate('onboarding.token.placeholder')} - required={true} - type="text" - value={tokenName || ''} - /> - {loading ? ( - <i className="spinner text-middle" /> - ) : ( - <button className="text-middle">{translate('onboarding.token.generate')}</button> - )} - </form> + <div> + {this.renderGenerateOption()} + {this.renderUseExistingOption()} + </div> )} <div className="note big-spacer-top width-50">{translate('onboarding.token.text')}</div> - {token != null && ( + {((selection === 'generate' && token != null) || + (selection === 'use-existing' && existingToken)) && ( <div className="big-spacer-top"> <button className="js-continue" onClick={this.handleContinueClick}> {translate('continue')} @@ -161,7 +240,8 @@ export default class TokenStep extends React.PureComponent { }; renderResult = () => { - const { token, tokenName } = this.state; + const { selection, tokenName } = this.state; + const token = this.getToken(); if (!token) { return null; @@ -170,8 +250,7 @@ export default class TokenStep extends React.PureComponent { return ( <div className="boxed-group-actions"> <i className="icon-check spacer-right" /> - {tokenName} - {': '} + {selection === 'generate' && tokenName && `${tokenName}: `} <strong>{token}</strong> </div> ); 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 8a4608bb7ab..2c2f07d5b75 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 @@ -77,3 +77,19 @@ it('continues', () => { click(wrapper.find('.js-continue')); expect(onContinue).toBeCalledWith('abcd1234'); }); + +it('uses existing token', () => { + const onContinue = jest.fn(); + const wrapper = mount( + <TokenStep + finished={false} + open={true} + onContinue={onContinue} + onOpen={jest.fn()} + stepNumber={1} + /> + ); + 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 f8234801320..3fed8ff9708 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 @@ -39,7 +39,7 @@ exports[`guides for on-premise 1`] = ` <div className="page-description" > - onboarding.header.description_x.2 + onboarding.header.description.2 </div> </header> <TokenStep @@ -99,7 +99,7 @@ exports[`guides for on-premise 2`] = ` <div className="page-description" > - onboarding.header.description_x.2 + onboarding.header.description.2 </div> </header> <TokenStep @@ -160,7 +160,7 @@ exports[`guides for sonarcloud 1`] = ` <div className="page-description" > - onboarding.header.description_x.3 + onboarding.header.description.3 </div> </header> <OrganizationStep @@ -233,7 +233,7 @@ exports[`guides for sonarcloud 2`] = ` <div className="page-description" > - onboarding.header.description_x.3 + onboarding.header.description.3 </div> </header> <OrganizationStep @@ -307,7 +307,7 @@ exports[`guides for sonarcloud 3`] = ` <div className="page-description" > - onboarding.header.description_x.3 + onboarding.header.description.3 </div> </header> <OrganizationStep 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 65138e0b498..38b83ffa20a 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 @@ -35,24 +35,57 @@ exports[`generates token 1`] = ` <div className="boxed-group-inner" > - <form - onSubmit={[Function]} - > - <input - autoFocus={true} - className="input-large spacer-right text-middle" - onChange={[Function]} - placeholder="onboarding.token.placeholder" - required={true} - type="text" - value="" - /> - <button - className="text-middle" + <div> + <div> + <a + className="js-new link-base-color link-no-underline" + href="#" + onClick={[Function]} + > + <i + className="icon-radio spacer-right is-checked" + /> + onboading.token.generate_token + </a> + <div + className="big-spacer-top" + > + <form + onSubmit={[Function]} + > + <input + autoFocus={true} + className="input-large spacer-right text-middle" + onChange={[Function]} + placeholder="onboading.token.generate_token.placeholder" + required={true} + type="text" + value="" + /> + <button + className="text-middle" + disabled={true} + > + onboarding.token.generate + </button> + </form> + </div> + </div> + <div + className="big-spacer-top" > - onboarding.token.generate - </button> - </form> + <a + className="js-new link-base-color link-no-underline" + href="#" + onClick={[Function]} + > + <i + className="icon-radio spacer-right" + /> + onboarding.token.use_existing_token + </a> + </div> + </div> <div className="note big-spacer-top width-50" > @@ -99,22 +132,54 @@ exports[`generates token 2`] = ` <div className="boxed-group-inner" > - <form - onSubmit={[Function]} - > - <input - autoFocus={true} - className="input-large spacer-right text-middle" - onChange={[Function]} - placeholder="onboarding.token.placeholder" - required={true} - type="text" - value="my token" - /> - <i - className="spinner text-middle" - /> - </form> + <div> + <div> + <a + className="js-new link-base-color link-no-underline" + href="#" + onClick={[Function]} + > + <i + className="icon-radio spacer-right is-checked" + /> + onboading.token.generate_token + </a> + <div + className="big-spacer-top" + > + <form + onSubmit={[Function]} + > + <input + autoFocus={true} + className="input-large spacer-right text-middle" + onChange={[Function]} + placeholder="onboading.token.generate_token.placeholder" + required={true} + type="text" + value="my token" + /> + <i + className="spinner text-middle" + /> + </form> + </div> + </div> + <div + className="big-spacer-top" + > + <a + className="js-new link-base-color link-no-underline" + href="#" + onClick={[Function]} + > + <i + className="icon-radio spacer-right" + /> + onboarding.token.use_existing_token + </a> + </div> + </div> <div className="note big-spacer-top width-50" > @@ -419,24 +484,57 @@ exports[`revokes token 3`] = ` <div className="boxed-group-inner" > - <form - onSubmit={[Function]} - > - <input - autoFocus={true} - className="input-large spacer-right text-middle" - onChange={[Function]} - placeholder="onboarding.token.placeholder" - required={true} - type="text" - value="" - /> - <button - className="text-middle" + <div> + <div> + <a + className="js-new link-base-color link-no-underline" + href="#" + onClick={[Function]} + > + <i + className="icon-radio spacer-right is-checked" + /> + onboading.token.generate_token + </a> + <div + className="big-spacer-top" + > + <form + onSubmit={[Function]} + > + <input + autoFocus={true} + className="input-large spacer-right text-middle" + onChange={[Function]} + placeholder="onboading.token.generate_token.placeholder" + required={true} + type="text" + value="" + /> + <button + className="text-middle" + disabled={true} + > + onboarding.token.generate + </button> + </form> + </div> + </div> + <div + className="big-spacer-top" > - onboarding.token.generate - </button> - </form> + <a + className="js-new link-base-color link-no-underline" + href="#" + onClick={[Function]} + > + <i + className="icon-radio spacer-right" + /> + onboarding.token.use_existing_token + </a> + </div> + </div> <div className="note big-spacer-top width-50" > 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 acd1bd356c3..2d914ebe6cf 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -2470,10 +2470,14 @@ onboarding.header=Welcome to SonarQube! onboarding.header.sonarcloud=Welcome to SonarCloud! onboarding.header.description=Want to quickly analyze a first project? Follow these {0} easy steps. -onboarding.token.header=Generate a token +onboarding.token.header=Provide a token onboarding.token.text=The token is used to identify you when an analysis is performed. If it has been compromised, you can revoke it at any point of time in your user account. onboarding.token.generate=Generate onboarding.token.placeholder=Enter a name for your token +onboading.token.generate_token=Generate a token +onboading.token.generate_token.placeholder=Enter a name for your token +onboarding.token.use_existing_token=Use existing token +onboarding.token.use_existing_token.placeholder=Enter your existing token onboarding.organization.header=Choose an organization for your project onboarding.organization.text=Organizations are where your projects belong. You can add your team members to your organization later to allow them to contribute to your projects. |