width: 10% !important;
}
+.abs-width-100 {
+ width: 100px !important;
+}
+
.abs-width-150 {
width: 150px !important;
}
+
.abs-width-240 {
width: 240px !important;
}
import { Alert } from '../../../components/ui/Alert';
import DeferredSpinner from '../../../components/ui/DeferredSpinner';
import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { TokenType } from '../../../types/token';
+import {
+ computeTokenExpirationDate,
+ EXPIRATION_OPTIONS,
+ getAvailableExpirationOptions
+} from '../../../helpers/tokens';
+import { TokenExpiration, TokenType } from '../../../types/token';
import { Component } from '../../../types/types';
import { LoggedInUser } from '../../../types/users';
+import Select from '../../controls/Select';
import { getUniqueTokenName } from '../utils';
interface State {
loading: boolean;
token?: string;
tokenName: string;
+ tokenExpiration: TokenExpiration;
+ tokenExpirationOptions: { value: TokenExpiration; label: string }[];
}
interface Props {
mounted = false;
state: State = {
loading: true,
- tokenName: ''
+ tokenName: '',
+ tokenExpiration: TokenExpiration.OneMonth,
+ tokenExpirationOptions: EXPIRATION_OPTIONS
};
componentDidMount() {
this.mounted = true;
this.getTokensAndName();
+ this.getTokenExpirationOptions();
}
componentWillUnmount() {
}
};
+ getTokenExpirationOptions = async () => {
+ const tokenExpirationOptions = await getAvailableExpirationOptions();
+ if (tokenExpirationOptions && this.mounted) {
+ this.setState({ tokenExpirationOptions });
+ }
+ };
+
getNewToken = async () => {
const {
component: { key }
} = this.props;
- const { tokenName } = this.state;
+ const { tokenName, tokenExpiration } = this.state;
const { token } = await generateToken({
name: tokenName,
type: TokenType.Project,
- projectKey: key
+ projectKey: key,
+ ...(tokenExpiration !== TokenExpiration.NoExpiration && {
+ expirationDate: computeTokenExpirationDate(tokenExpiration)
+ })
});
if (this.mounted) {
}
};
- handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+ handleTokenNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({
tokenName: event.target.value
});
};
+ handleTokenExpirationChange = ({ value }: { value: TokenExpiration }) => {
+ this.setState({ tokenExpiration: value });
+ };
+
handleTokenRevoke = async () => {
const { tokenName } = this.state;
};
render() {
- const { loading, token, tokenName } = this.state;
+ const { loading, token, tokenName, tokenExpiration, tokenExpirationOptions } = this.state;
const header = translate('onboarding.token.generate_project_token');
</Alert>
</>
) : (
- <div className="big-spacer-top">
+ <div className="big-spacer-top display-flex-center">
{loading ? (
<DeferredSpinner />
) : (
<>
- <input
- className="input-super-large spacer-right text-middle"
- onChange={this.handleChange}
- placeholder={translate('onboarding.token.generate_token.placeholder')}
- required={true}
- type="text"
- value={tokenName}
- />
- <Button
- className="text-middle"
- disabled={!tokenName}
- onClick={this.getNewToken}>
- {translate('onboarding.token.generate')}
- </Button>
+ <div className="display-flex-column">
+ <label className="text-bold little-spacer-bottom" htmlFor="token-name">
+ {translate('onboarding.token.generate_token.placeholder')}
+ </label>
+ <input
+ className="input-large spacer-right text-middle"
+ onChange={this.handleTokenNameChange}
+ required={true}
+ id="token-name"
+ type="text"
+ value={tokenName}
+ />
+ </div>
+ <div className="display-flex-column">
+ <label
+ className="text-bold little-spacer-bottom"
+ htmlFor="token-expiration">
+ {translate('users.tokens.expires_in')}
+ </label>
+ <div className="display-flex-center">
+ <Select
+ id="token-expiration"
+ className="abs-width-100 spacer-right"
+ isSearchable={false}
+ onChange={this.handleTokenExpirationChange}
+ options={tokenExpirationOptions}
+ value={tokenExpirationOptions.find(
+ option => option.value === tokenExpiration
+ )}
+ />
+ <Button
+ className="text-middle"
+ disabled={!tokenName}
+ onClick={this.getNewToken}>
+ {translate('onboarding.token.generate')}
+ </Button>
+ </div>
+ </div>
</>
)}
</div>
getUniqueTokenName: jest.fn().mockReturnValue('lightsaber-9000')
}));
+jest.mock('../../../../api/settings', () => {
+ return {
+ ...jest.requireActual('../../../../api/settings'),
+ getAllValues: jest.fn().mockResolvedValue([
+ {
+ key: 'sonar.auth.token.max.allowed.lifetime',
+ value: 'No expiration'
+ }
+ ])
+ };
+});
+
beforeEach(() => {
jest.clearAllMocks();
});
const wrapper = shallowRender();
const instance = wrapper.instance();
- instance.handleChange(mockEvent({ target: { value: 'my-token' } }));
+ instance.handleTokenNameChange(mockEvent({ target: { value: 'my-token' } }));
expect(wrapper.state('tokenName')).toBe('my-token');
});
import Radio from '../../../components/controls/Radio';
import AlertSuccessIcon from '../../../components/icons/AlertSuccessIcon';
import { translate } from '../../../helpers/l10n';
-import { TokenType, UserToken } from '../../../types/token';
+import {
+ computeTokenExpirationDate,
+ EXPIRATION_OPTIONS,
+ getAvailableExpirationOptions
+} from '../../../helpers/tokens';
+import { TokenExpiration, TokenType, UserToken } from '../../../types/token';
import { LoggedInUser } from '../../../types/users';
import DocumentationTooltip from '../../common/DocumentationTooltip';
+import Select from '../../controls/Select';
import AlertErrorIcon from '../../icons/AlertErrorIcon';
import Step from '../components/Step';
import { getUniqueTokenName } from '../utils';
tokenName?: string;
token?: string;
tokens?: UserToken[];
+ tokenExpiration: TokenExpiration;
+ tokenExpirationOptions: { value: TokenExpiration; label: string }[];
}
const TOKEN_FORMAT_REGEX = /^[_a-z0-9]+$/;
existingToken: '',
loading: false,
selection: 'generate',
- tokenName: props.initialTokenName
+ tokenName: props.initialTokenName,
+ tokenExpiration: TokenExpiration.OneMonth,
+ tokenExpirationOptions: EXPIRATION_OPTIONS
};
}
const { currentUser, initialTokenName } = this.props;
const { tokenName } = this.state;
+ const tokenExpirationOptions = await getAvailableExpirationOptions();
+ if (tokenExpirationOptions && this.mounted) {
+ this.setState({ tokenExpirationOptions });
+ }
+
const tokens = await getTokens(currentUser.login).catch(() => {
/* noop */
});
this.setState({ tokenName: event.target.value });
};
+ handleTokenExpirationChange = ({ value }: { value: TokenExpiration }) => {
+ this.setState({ tokenExpiration: value });
+ };
+
handleTokenGenerate = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
- const { tokenName } = this.state;
+ const { tokenName, tokenExpiration } = this.state;
const { projectKey } = this.props;
if (tokenName) {
const { token } = await generateToken({
name: tokenName,
type: TokenType.Project,
- projectKey
+ projectKey,
+ ...(tokenExpiration !== TokenExpiration.NoExpiration && {
+ expirationDate: computeTokenExpirationDate(tokenExpiration)
+ })
});
if (this.mounted) {
this.setState({ loading: false, token });
}
};
- renderGenerateOption = () => (
- <div>
- {this.state.tokens !== undefined && this.state.tokens.length > 0 ? (
- <Radio
- checked={this.state.selection === 'generate'}
- onCheck={this.handleModeChange}
- value="generate">
- {translate('onboarding.token.generate_project_token')}
- </Radio>
- ) : (
- translate('onboarding.token.generate_project_token')
- )}
- {this.state.selection === 'generate' && (
- <div className="big-spacer-top">
- <form className="display-flex-column" onSubmit={this.handleTokenGenerate}>
- <label className="h3" htmlFor="generate-token-input">
- {translate('onboarding.token.generate_project_token.label')}
- <DocumentationTooltip
- className="spacer-left"
- content={translate('onboarding.token.generate_project_token.help')}
- links={[
- {
- href: '/documentation/user-guide/user-token/',
- label: translate('learn_more')
- }
- ]}
- />
- </label>
- <div>
- <input
- id="generate-token-input"
- autoFocus={true}
- className="input-super-large spacer-right spacer-top text-middle"
- onChange={this.handleTokenNameChange}
- required={true}
- type="text"
- value={this.state.tokenName || ''}
- />
- {this.state.loading ? (
- <i className="spinner text-middle" />
- ) : (
- <SubmitButton className="text-middle spacer-top" disabled={!this.state.tokenName}>
- {translate('onboarding.token.generate')}
- </SubmitButton>
- )}
- </div>
- </form>
- </div>
- )}
- </div>
- );
+ renderGenerateOption = () => {
+ const {
+ loading,
+ selection,
+ tokens,
+ tokenName,
+ tokenExpiration,
+ tokenExpirationOptions
+ } = this.state;
+ return (
+ <div>
+ {tokens !== undefined && tokens.length > 0 ? (
+ <Radio
+ checked={selection === 'generate'}
+ onCheck={this.handleModeChange}
+ value="generate">
+ {translate('onboarding.token.generate_project_token')}
+ </Radio>
+ ) : (
+ translate('onboarding.token.generate_project_token')
+ )}
+ {selection === 'generate' && (
+ <div className="big-spacer-top">
+ <form className="display-flex-center" onSubmit={this.handleTokenGenerate}>
+ <div className="display-flex-column">
+ <label className="h3" htmlFor="generate-token-input">
+ {translate('onboarding.token.generate_project_token.label')}
+ <DocumentationTooltip
+ className="spacer-left"
+ content={translate('onboarding.token.generate_project_token.help')}
+ links={[
+ {
+ href: '/documentation/user-guide/user-token/',
+ label: translate('learn_more')
+ }
+ ]}
+ />
+ </label>
+ <input
+ id="generate-token-input"
+ autoFocus={true}
+ className="input-super-large spacer-right spacer-top text-middle"
+ onChange={this.handleTokenNameChange}
+ required={true}
+ type="text"
+ value={tokenName || ''}
+ />
+ </div>
+ <div className="display-flex-column spacer-left big-spacer-right">
+ <label htmlFor="token-select-expiration" className="h3">
+ {translate('users.tokens.expires_in')}
+ </label>
+ <div className="display-flex-center">
+ <Select
+ id="token-select-expiration"
+ className="spacer-top abs-width-100 spacer-right"
+ isSearchable={false}
+ onChange={this.handleTokenExpirationChange}
+ options={tokenExpirationOptions}
+ value={tokenExpirationOptions.find(option => option.value === tokenExpiration)}
+ />
+
+ {loading ? (
+ <i className="spinner text-middle" />
+ ) : (
+ <SubmitButton className="text-middle spacer-top" disabled={!tokenName}>
+ {translate('onboarding.token.generate')}
+ </SubmitButton>
+ )}
+ </div>
+ </div>
+ </form>
+ </div>
+ )}
+ </div>
+ );
+ };
renderUseExistingOption = () => {
const { existingToken } = this.state;
revokeToken: jest.fn().mockResolvedValue(null)
}));
+jest.mock('../../../../api/settings', () => {
+ return {
+ ...jest.requireActual('../../../../api/settings'),
+ getAllValues: jest.fn().mockResolvedValue([
+ {
+ key: 'sonar.auth.token.max.allowed.lifetime',
+ value: 'No expiration'
+ }
+ ])
+ };
+});
+
it('sets an initial token name', async () => {
(getTokens as jest.Mock).mockResolvedValueOnce([{ name: 'fôo' }]);
const wrapper = shallowRender({ initialTokenName: 'fôo' });
className="big-spacer-top"
>
<form
- className="display-flex-column"
+ className="display-flex-center"
onSubmit={[Function]}
>
- <label
- className="h3"
- htmlFor="generate-token-input"
+ <div
+ className="display-flex-column"
>
- onboarding.token.generate_project_token.label
- <DocumentationTooltip
- className="spacer-left"
- content="onboarding.token.generate_project_token.help"
- links={
- Array [
- Object {
- "href": "/documentation/user-guide/user-token/",
- "label": "learn_more",
- },
- ]
- }
- />
- </label>
- <div>
+ <label
+ className="h3"
+ htmlFor="generate-token-input"
+ >
+ onboarding.token.generate_project_token.label
+ <DocumentationTooltip
+ className="spacer-left"
+ content="onboarding.token.generate_project_token.help"
+ links={
+ Array [
+ Object {
+ "href": "/documentation/user-guide/user-token/",
+ "label": "learn_more",
+ },
+ ]
+ }
+ />
+ </label>
<input
autoFocus={true}
className="input-super-large spacer-right spacer-top text-middle"
type="text"
value=""
/>
- <SubmitButton
- className="text-middle spacer-top"
- disabled={true}
+ </div>
+ <div
+ className="display-flex-column spacer-left big-spacer-right"
+ >
+ <label
+ className="h3"
+ htmlFor="token-select-expiration"
+ >
+ users.tokens.expires_in
+ </label>
+ <div
+ className="display-flex-center"
>
- onboarding.token.generate
- </SubmitButton>
+ <Select
+ className="spacer-top abs-width-100 spacer-right"
+ id="token-select-expiration"
+ isSearchable={false}
+ onChange={[Function]}
+ options={
+ Array [
+ Object {
+ "label": "users.tokens.expiration.30",
+ "value": 30,
+ },
+ Object {
+ "label": "users.tokens.expiration.90",
+ "value": 90,
+ },
+ Object {
+ "label": "users.tokens.expiration.365",
+ "value": 365,
+ },
+ Object {
+ "label": "users.tokens.expiration.0",
+ "value": 0,
+ },
+ ]
+ }
+ value={
+ Object {
+ "label": "users.tokens.expiration.30",
+ "value": 30,
+ }
+ }
+ />
+ <SubmitButton
+ className="text-middle spacer-top"
+ disabled={true}
+ >
+ onboarding.token.generate
+ </SubmitButton>
+ </div>
</div>
</form>
</div>
className="big-spacer-top"
>
<form
- className="display-flex-column"
+ className="display-flex-center"
onSubmit={[Function]}
>
- <label
- className="h3"
- htmlFor="generate-token-input"
+ <div
+ className="display-flex-column"
>
- onboarding.token.generate_project_token.label
- <DocumentationTooltip
- className="spacer-left"
- content="onboarding.token.generate_project_token.help"
- links={
- Array [
- Object {
- "href": "/documentation/user-guide/user-token/",
- "label": "learn_more",
- },
- ]
- }
- />
- </label>
- <div>
+ <label
+ className="h3"
+ htmlFor="generate-token-input"
+ >
+ onboarding.token.generate_project_token.label
+ <DocumentationTooltip
+ className="spacer-left"
+ content="onboarding.token.generate_project_token.help"
+ links={
+ Array [
+ Object {
+ "href": "/documentation/user-guide/user-token/",
+ "label": "learn_more",
+ },
+ ]
+ }
+ />
+ </label>
<input
autoFocus={true}
className="input-super-large spacer-right spacer-top text-middle"
type="text"
value="my token"
/>
- <i
- className="spinner text-middle"
- />
+ </div>
+ <div
+ className="display-flex-column spacer-left big-spacer-right"
+ >
+ <label
+ className="h3"
+ htmlFor="token-select-expiration"
+ >
+ users.tokens.expires_in
+ </label>
+ <div
+ className="display-flex-center"
+ >
+ <Select
+ className="spacer-top abs-width-100 spacer-right"
+ id="token-select-expiration"
+ isSearchable={false}
+ onChange={[Function]}
+ options={
+ Array [
+ Object {
+ "label": "users.tokens.expiration.30",
+ "value": 30,
+ },
+ Object {
+ "label": "users.tokens.expiration.90",
+ "value": 90,
+ },
+ Object {
+ "label": "users.tokens.expiration.365",
+ "value": 365,
+ },
+ Object {
+ "label": "users.tokens.expiration.0",
+ "value": 0,
+ },
+ ]
+ }
+ value={
+ Object {
+ "label": "users.tokens.expiration.30",
+ "value": 30,
+ }
+ }
+ />
+ <i
+ className="spinner text-middle"
+ />
+ </div>
</div>
</form>
</div>
className="big-spacer-top"
>
<form
- className="display-flex-column"
+ className="display-flex-center"
onSubmit={[Function]}
>
- <label
- className="h3"
- htmlFor="generate-token-input"
+ <div
+ className="display-flex-column"
>
- onboarding.token.generate_project_token.label
- <DocumentationTooltip
- className="spacer-left"
- content="onboarding.token.generate_project_token.help"
- links={
- Array [
- Object {
- "href": "/documentation/user-guide/user-token/",
- "label": "learn_more",
- },
- ]
- }
- />
- </label>
- <div>
+ <label
+ className="h3"
+ htmlFor="generate-token-input"
+ >
+ onboarding.token.generate_project_token.label
+ <DocumentationTooltip
+ className="spacer-left"
+ content="onboarding.token.generate_project_token.help"
+ links={
+ Array [
+ Object {
+ "href": "/documentation/user-guide/user-token/",
+ "label": "learn_more",
+ },
+ ]
+ }
+ />
+ </label>
<input
autoFocus={true}
className="input-super-large spacer-right spacer-top text-middle"
type="text"
value=""
/>
- <SubmitButton
- className="text-middle spacer-top"
- disabled={true}
+ </div>
+ <div
+ className="display-flex-column spacer-left big-spacer-right"
+ >
+ <label
+ className="h3"
+ htmlFor="token-select-expiration"
>
- onboarding.token.generate
- </SubmitButton>
+ users.tokens.expires_in
+ </label>
+ <div
+ className="display-flex-center"
+ >
+ <Select
+ className="spacer-top abs-width-100 spacer-right"
+ id="token-select-expiration"
+ isSearchable={false}
+ onChange={[Function]}
+ options={
+ Array [
+ Object {
+ "label": "users.tokens.expiration.30",
+ "value": 30,
+ },
+ Object {
+ "label": "users.tokens.expiration.90",
+ "value": 90,
+ },
+ Object {
+ "label": "users.tokens.expiration.365",
+ "value": 365,
+ },
+ Object {
+ "label": "users.tokens.expiration.0",
+ "value": 0,
+ },
+ ]
+ }
+ value={
+ Object {
+ "label": "users.tokens.expiration.30",
+ "value": 30,
+ }
+ }
+ />
+ <SubmitButton
+ className="text-middle spacer-top"
+ disabled={true}
+ >
+ onboarding.token.generate
+ </SubmitButton>
+ </div>
</div>
</form>
</div>
onboarding.token.generate=Generate
onboarding.token.placeholder=Enter a name for your token
onboarding.token.generate_token=Generate a token
+onboarding.token.generate_token.placeholder=Token name
onboarding.token.generate_project_token=Generate a project token
onboarding.token.generate_project_token.label=Token name
onboarding.token.use_existing_token=Use existing token