Browse Source

pre-validate exising token during onboarding

tags/6.7-RC1
Stas Vilchik 6 years ago
parent
commit
e462c69f76

+ 1
- 0
server/sonar-web/src/main/js/apps/tutorials/onboarding/Onboarding.js View File

@@ -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}

+ 79
- 44
server/sonar-web/src/main/js/apps/tutorials/onboarding/TokenStep.js View File

@@ -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')}

+ 15
- 4
server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/TokenStep-test.js View File

@@ -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');

+ 30
- 0
server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/Onboarding-test.js.snap View File

@@ -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]}

+ 30
- 0
server/sonar-web/src/main/js/apps/tutorials/onboarding/__tests__/__snapshots__/TokenStep-test.js.snap View File

@@ -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]}

+ 1
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -2432,6 +2432,7 @@ 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.token.invalid_format=The token you have entered has invalid format.

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.

Loading…
Cancel
Save