*/ | */ | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import InstanceMessage from '../../../components/common/InstanceMessage'; | import InstanceMessage from '../../../components/common/InstanceMessage'; | ||||
import TokenForm from '../../users/components/TokensForm'; | |||||
import TokensForm from '../../users/components/TokensForm'; | |||||
import { translate } from '../../../helpers/l10n'; | import { translate } from '../../../helpers/l10n'; | ||||
interface Props { | interface Props { | ||||
<InstanceMessage message={translate('my_account.tokens_description')} /> | <InstanceMessage message={translate('my_account.tokens_description')} /> | ||||
</div> | </div> | ||||
<TokenForm login={login} /> | |||||
<TokensForm deleteConfirmation="modal" login={login} /> | |||||
</div> | </div> | ||||
</div> | </div> | ||||
); | ); |
/> | /> | ||||
</div> | </div> | ||||
<TokensForm | <TokensForm | ||||
deleteConfirmation="modal" | |||||
login="user" | login="user" | ||||
/> | /> | ||||
</div> | </div> |
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||||
*/ | */ | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import TokensFormItem from './TokensFormItem'; | |||||
import TokensFormItem, { TokenDeleteConfirmation } from './TokensFormItem'; | |||||
import TokensFormNewToken from './TokensFormNewToken'; | import TokensFormNewToken from './TokensFormNewToken'; | ||||
import DeferredSpinner from '../../../components/common/DeferredSpinner'; | import DeferredSpinner from '../../../components/common/DeferredSpinner'; | ||||
import { SubmitButton } from '../../../components/ui/buttons'; | import { SubmitButton } from '../../../components/ui/buttons'; | ||||
import { translate } from '../../../helpers/l10n'; | import { translate } from '../../../helpers/l10n'; | ||||
interface Props { | interface Props { | ||||
deleteConfirmation: TokenDeleteConfirmation; | |||||
login: string; | login: string; | ||||
updateTokensCount?: (login: string, tokensCount: number) => void; | updateTokensCount?: (login: string, tokensCount: number) => void; | ||||
} | } | ||||
} | } | ||||
return tokens.map(token => ( | return tokens.map(token => ( | ||||
<TokensFormItem | <TokensFormItem | ||||
deleteConfirmation={this.props.deleteConfirmation} | |||||
key={token.name} | key={token.name} | ||||
login={this.props.login} | login={this.props.login} | ||||
onRevokeToken={this.handleRevokeToken} | onRevokeToken={this.handleRevokeToken} | ||||
{newToken && <TokensFormNewToken token={newToken} />} | {newToken && <TokensFormNewToken token={newToken} />} | ||||
<table className="data zebra big-spacer-top "> | |||||
<table className="data zebra big-spacer-top"> | |||||
<thead> | <thead> | ||||
<tr> | <tr> | ||||
<th>{translate('name')}</th> | <th>{translate('name')}</th> |
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||||
*/ | */ | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { FormattedMessage } from 'react-intl'; | |||||
import { Button } from '../../../components/ui/buttons'; | |||||
import ConfirmButton from '../../../components/controls/ConfirmButton'; | |||||
import DateFormatter from '../../../components/intl/DateFormatter'; | import DateFormatter from '../../../components/intl/DateFormatter'; | ||||
import DateFromNowHourPrecision from '../../../components/intl/DateFromNowHourPrecision'; | import DateFromNowHourPrecision from '../../../components/intl/DateFromNowHourPrecision'; | ||||
import DeferredSpinner from '../../../components/common/DeferredSpinner'; | import DeferredSpinner from '../../../components/common/DeferredSpinner'; | ||||
import Tooltip from '../../../components/controls/Tooltip'; | import Tooltip from '../../../components/controls/Tooltip'; | ||||
import { Button } from '../../../components/ui/buttons'; | |||||
import { limitComponentName } from '../../../helpers/path'; | import { limitComponentName } from '../../../helpers/path'; | ||||
import { revokeToken } from '../../../api/user-tokens'; | import { revokeToken } from '../../../api/user-tokens'; | ||||
import { translate } from '../../../helpers/l10n'; | import { translate } from '../../../helpers/l10n'; | ||||
export type TokenDeleteConfirmation = 'inline' | 'modal'; | |||||
interface Props { | interface Props { | ||||
deleteConfirmation: TokenDeleteConfirmation; | |||||
login: string; | login: string; | ||||
onRevokeToken: (token: T.UserToken) => void; | onRevokeToken: (token: T.UserToken) => void; | ||||
token: T.UserToken; | token: T.UserToken; | ||||
} | } | ||||
interface State { | interface State { | ||||
deleting: boolean; | |||||
loading: boolean; | loading: boolean; | ||||
showConfirmation: boolean; | |||||
} | } | ||||
export default class TokensFormItem extends React.PureComponent<Props, State> { | export default class TokensFormItem extends React.PureComponent<Props, State> { | ||||
mounted = false; | mounted = false; | ||||
state: State = { deleting: false, loading: false }; | |||||
state: State = { loading: false, showConfirmation: false }; | |||||
componentDidMount() { | componentDidMount() { | ||||
this.mounted = true; | this.mounted = true; | ||||
this.mounted = false; | this.mounted = false; | ||||
} | } | ||||
handleRevoke = () => { | |||||
if (this.state.deleting) { | |||||
this.setState({ loading: true }); | |||||
revokeToken({ login: this.props.login, name: this.props.token.name }).then( | |||||
() => this.props.onRevokeToken(this.props.token), | |||||
() => { | |||||
if (this.mounted) { | |||||
this.setState({ loading: false, deleting: false }); | |||||
} | |||||
handleClick = () => { | |||||
if (this.state.showConfirmation) { | |||||
this.handleRevoke().then(() => { | |||||
if (this.mounted) { | |||||
this.setState({ showConfirmation: false }); | |||||
} | } | ||||
); | |||||
}); | |||||
} else { | } else { | ||||
this.setState({ deleting: true }); | |||||
this.setState({ showConfirmation: true }); | |||||
} | } | ||||
}; | }; | ||||
handleRevoke = () => { | |||||
this.setState({ loading: true }); | |||||
return revokeToken({ login: this.props.login, name: this.props.token.name }).then( | |||||
() => this.props.onRevokeToken(this.props.token), | |||||
() => { | |||||
if (this.mounted) { | |||||
this.setState({ loading: false }); | |||||
} | |||||
} | |||||
); | |||||
}; | |||||
render() { | render() { | ||||
const { token } = this.props; | |||||
const { loading } = this.state; | |||||
const { deleteConfirmation, token } = this.props; | |||||
const { loading, showConfirmation } = this.state; | |||||
return ( | return ( | ||||
<tr> | <tr> | ||||
<td> | <td> | ||||
<DeferredSpinner loading={loading}> | <DeferredSpinner loading={loading}> | ||||
<i className="spinner-placeholder" /> | <i className="spinner-placeholder" /> | ||||
</DeferredSpinner> | </DeferredSpinner> | ||||
<Button | |||||
className="button-red input-small spacer-left" | |||||
disabled={loading} | |||||
onClick={this.handleRevoke}> | |||||
{this.state.deleting | |||||
? translate('users.tokens.sure') | |||||
: translate('users.tokens.revoke')} | |||||
</Button> | |||||
{deleteConfirmation === 'modal' ? ( | |||||
<ConfirmButton | |||||
confirmButtonText={translate('users.tokens.revoke_token')} | |||||
isDestructive={true} | |||||
modalBody={ | |||||
<FormattedMessage | |||||
defaultMessage={translate('users.tokens.sure_X')} | |||||
id="users.tokens.sure_X" | |||||
values={{ token: <strong>{token.name}</strong> }} | |||||
/> | |||||
} | |||||
modalHeader={translate('users.tokens.revoke_token')} | |||||
onConfirm={this.handleRevoke}> | |||||
{({ onClick }) => ( | |||||
<Button | |||||
className="spacer-left button-red input-small" | |||||
disabled={loading} | |||||
onClick={onClick} | |||||
title={translate('users.tokens.revoke_token')}> | |||||
{translate('users.tokens.revoke')} | |||||
</Button> | |||||
)} | |||||
</ConfirmButton> | |||||
) : ( | |||||
<Button | |||||
className="button-red input-small spacer-left" | |||||
disabled={loading} | |||||
onClick={this.handleClick}> | |||||
{showConfirmation ? translate('users.tokens.sure') : translate('users.tokens.revoke')} | |||||
</Button> | |||||
)} | |||||
</td> | </td> | ||||
</tr> | </tr> | ||||
); | ); |
</h2> | </h2> | ||||
</header> | </header> | ||||
<div className="modal-body modal-container"> | <div className="modal-body modal-container"> | ||||
<TokensForm login={props.user.login} updateTokensCount={props.updateTokensCount} /> | |||||
<TokensForm | |||||
deleteConfirmation="inline" | |||||
login={props.user.login} | |||||
updateTokensCount={props.updateTokensCount} | |||||
/> | |||||
</div> | </div> | ||||
<footer className="modal-foot"> | <footer className="modal-foot"> | ||||
<ResetButtonLink onClick={props.onClose}>{translate('Done')}</ResetButtonLink> | <ResetButtonLink onClick={props.onClose}>{translate('Done')}</ResetButtonLink> |
}); | }); | ||||
function shallowRender(props: Partial<TokensForm['props']> = {}) { | function shallowRender(props: Partial<TokensForm['props']> = {}) { | ||||
return shallow<TokensForm>(<TokensForm login="luke" updateTokensCount={jest.fn()} {...props} />); | |||||
return shallow<TokensForm>( | |||||
<TokensForm deleteConfirmation="inline" login="luke" updateTokensCount={jest.fn()} {...props} /> | |||||
); | |||||
} | } |
it('should render correctly', () => { | it('should render correctly', () => { | ||||
expect(shallowRender()).toMatchSnapshot(); | expect(shallowRender()).toMatchSnapshot(); | ||||
expect(shallowRender({ deleteConfirmation: 'modal' })).toMatchSnapshot(); | |||||
}); | }); | ||||
it('should revoke the token', async () => { | |||||
it('should revoke the token using inline confirmation', async () => { | |||||
const onRevokeToken = jest.fn(); | const onRevokeToken = jest.fn(); | ||||
const wrapper = shallowRender({ onRevokeToken }); | |||||
const wrapper = shallowRender({ deleteConfirmation: 'inline', onRevokeToken }); | |||||
expect(wrapper.find('Button')).toMatchSnapshot(); | expect(wrapper.find('Button')).toMatchSnapshot(); | ||||
click(wrapper.find('Button')); | click(wrapper.find('Button')); | ||||
expect(wrapper.find('Button')).toMatchSnapshot(); | expect(wrapper.find('Button')).toMatchSnapshot(); | ||||
expect(onRevokeToken).toHaveBeenCalledWith(userToken); | expect(onRevokeToken).toHaveBeenCalledWith(userToken); | ||||
}); | }); | ||||
it('should revoke the token using modal confirmation', async () => { | |||||
const onRevokeToken = jest.fn(); | |||||
const wrapper = shallowRender({ deleteConfirmation: 'modal', onRevokeToken }); | |||||
wrapper.find('ConfirmButton').prop<Function>('onConfirm')(); | |||||
expect(revokeToken).toHaveBeenCalledWith({ login: 'luke', name: 'foo' }); | |||||
await waitAndUpdate(wrapper); | |||||
expect(onRevokeToken).toHaveBeenCalledWith(userToken); | |||||
}); | |||||
function shallowRender(props: Partial<TokensFormItem['props']> = {}) { | function shallowRender(props: Partial<TokensFormItem['props']> = {}) { | ||||
return shallow( | return shallow( | ||||
<TokensFormItem login="luke" onRevokeToken={jest.fn()} token={userToken} {...props} /> | |||||
<TokensFormItem | |||||
deleteConfirmation="inline" | |||||
login="luke" | |||||
onRevokeToken={jest.fn()} | |||||
token={userToken} | |||||
{...props} | |||||
/> | |||||
); | ); | ||||
} | } |
</SubmitButton> | </SubmitButton> | ||||
</form> | </form> | ||||
<table | <table | ||||
className="data zebra big-spacer-top " | |||||
className="data zebra big-spacer-top" | |||||
> | > | ||||
<thead> | <thead> | ||||
<tr> | <tr> | ||||
</SubmitButton> | </SubmitButton> | ||||
</form> | </form> | ||||
<table | <table | ||||
className="data zebra big-spacer-top " | |||||
className="data zebra big-spacer-top" | |||||
> | > | ||||
<thead> | <thead> | ||||
<tr> | <tr> | ||||
timeout={100} | timeout={100} | ||||
> | > | ||||
<TokensFormItem | <TokensFormItem | ||||
deleteConfirmation="inline" | |||||
key="foo" | key="foo" | ||||
login="luke" | login="luke" | ||||
onRevokeToken={[Function]} | onRevokeToken={[Function]} | ||||
} | } | ||||
/> | /> | ||||
<TokensFormItem | <TokensFormItem | ||||
deleteConfirmation="inline" | |||||
key="bar" | key="bar" | ||||
login="luke" | login="luke" | ||||
onRevokeToken={[Function]} | onRevokeToken={[Function]} |
</tr> | </tr> | ||||
`; | `; | ||||
exports[`should revoke the token 1`] = ` | |||||
exports[`should render correctly 2`] = ` | |||||
<tr> | |||||
<td> | |||||
<Tooltip | |||||
overlay="foo" | |||||
> | |||||
<span> | |||||
foo | |||||
</span> | |||||
</Tooltip> | |||||
</td> | |||||
<td | |||||
className="nowrap" | |||||
> | |||||
<DateFromNowHourPrecision | |||||
date="2019-01-18T15:06:33+0100" | |||||
/> | |||||
</td> | |||||
<td | |||||
className="thin nowrap text-right" | |||||
> | |||||
<DateFormatter | |||||
date="2019-01-15T15:06:33+0100" | |||||
long={true} | |||||
/> | |||||
</td> | |||||
<td | |||||
className="thin nowrap text-right" | |||||
> | |||||
<DeferredSpinner | |||||
loading={false} | |||||
timeout={100} | |||||
> | |||||
<i | |||||
className="spinner-placeholder" | |||||
/> | |||||
</DeferredSpinner> | |||||
<ConfirmButton | |||||
confirmButtonText="users.tokens.revoke_token" | |||||
isDestructive={true} | |||||
modalBody={ | |||||
<FormattedMessage | |||||
defaultMessage="users.tokens.sure_X" | |||||
id="users.tokens.sure_X" | |||||
values={ | |||||
Object { | |||||
"token": <strong> | |||||
foo | |||||
</strong>, | |||||
} | |||||
} | |||||
/> | |||||
} | |||||
modalHeader="users.tokens.revoke_token" | |||||
onConfirm={[Function]} | |||||
> | |||||
<Component /> | |||||
</ConfirmButton> | |||||
</td> | |||||
</tr> | |||||
`; | |||||
exports[`should revoke the token using inline confirmation 1`] = ` | |||||
<Button | <Button | ||||
className="button-red input-small spacer-left" | className="button-red input-small spacer-left" | ||||
disabled={false} | disabled={false} | ||||
</Button> | </Button> | ||||
`; | `; | ||||
exports[`should revoke the token 2`] = ` | |||||
exports[`should revoke the token using inline confirmation 2`] = ` | |||||
<Button | <Button | ||||
className="button-red input-small spacer-left" | className="button-red input-small spacer-left" | ||||
disabled={false} | disabled={false} |
className="modal-body modal-container" | className="modal-body modal-container" | ||||
> | > | ||||
<TokensForm | <TokensForm | ||||
deleteConfirmation="inline" | |||||
login="john.doe" | login="john.doe" | ||||
updateTokensCount={[MockFunction]} | updateTokensCount={[MockFunction]} | ||||
/> | /> |
users.search_description=Search users by login or name | users.search_description=Search users by login or name | ||||
users.update=Update users | users.update=Update users | ||||
users.tokens=Tokens | users.tokens=Tokens | ||||
users.user_X_tokens=Tokens of {user} | |||||
users.tokens.sure=Sure? | users.tokens.sure=Sure? | ||||
users.tokens.sure_X=Are you sure you want to revoke token {token}? | |||||
users.tokens.revoke=Revoke | users.tokens.revoke=Revoke | ||||
users.user_X_tokens=Tokens of {user} | |||||
users.tokens.revoke_token=Revoke token | |||||
users.no_tokens=No tokens | users.no_tokens=No tokens | ||||
users.generate=Generate | users.generate=Generate | ||||
users.generate_tokens=Generate Tokens | users.generate_tokens=Generate Tokens |