-
Notifications
You must be signed in to change notification settings - Fork 11.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[NEW] Personal Access Tokens rewritten (#18206)
* Finished refactor accountTokens * Use useSetModal hook * Prefer children over renderRow on GenericTable * Fix AddToken markup * Move InfoModal to its own module * Move hook calls into inner components * Block access to AccountTokensPage rendering Co-authored-by: Tasso Evangelista <[email protected]>
- Loading branch information
1 parent
13c1d88
commit 19001a4
Showing
8 changed files
with
273 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import React from 'react'; | ||
|
||
import { useTranslation } from '../../contexts/TranslationContext'; | ||
import { useEndpointDataExperimental } from '../../hooks/useEndpointDataExperimental'; | ||
import Page from '../../components/basic/Page'; | ||
import AccountTokensTable from './AccountTokensTable'; | ||
import AddToken from './AddToken'; | ||
|
||
const AccountTokensPage = () => { | ||
const t = useTranslation(); | ||
const { data, reload } = useEndpointDataExperimental('users.getPersonalAccessTokens'); | ||
|
||
return <Page> | ||
<Page.Header title={t('Personal_Access_Tokens')}/> | ||
<Page.Content> | ||
<AddToken onDidAddToken={reload}/> | ||
<AccountTokensTable data={data} reload={reload} /> | ||
</Page.Content> | ||
</Page>; | ||
}; | ||
|
||
export default AccountTokensPage; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
import { Table, Button, ButtonGroup, Icon, Box } from '@rocket.chat/fuselage'; | ||
import React, { useMemo, useCallback, useState } from 'react'; | ||
|
||
import { GenericTable, Th } from '../../components/GenericTable'; | ||
import { useSetModal } from '../../contexts/ModalContext'; | ||
import { useMethod } from '../../contexts/ServerContext'; | ||
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext'; | ||
import { useTranslation } from '../../contexts/TranslationContext'; | ||
import { useResizeInlineBreakpoint } from '../../hooks/useResizeInlineBreakpoint'; | ||
import { useFormatDateAndTime } from '../../hooks/useFormatDateAndTime'; | ||
import InfoModal from './InfoModal'; | ||
import { useUserId } from '../../contexts/UserContext'; | ||
|
||
const TokenRow = ({ lastTokenPart, name, createdAt, bypassTwoFactor, formatDateAndTime, onRegenerate, onRemove, t, isMedium }) => { | ||
const handleRegenerate = useCallback(() => onRegenerate(name), [name, onRegenerate]); | ||
const handleRemove = useCallback(() => onRemove(name), [name, onRemove]); | ||
|
||
return <Table.Row key={name} tabIndex={0} role='link' action qa-token-name={name}> | ||
<Table.Cell withTruncatedText color='default' fontScale='p2'>{name}</Table.Cell> | ||
{isMedium && <Table.Cell withTruncatedText>{formatDateAndTime(createdAt)}</Table.Cell>} | ||
<Table.Cell withTruncatedText>...{lastTokenPart}</Table.Cell> | ||
<Table.Cell withTruncatedText>{bypassTwoFactor ? t('Ignore') : t('Require')}</Table.Cell> | ||
<Table.Cell withTruncatedText> | ||
<ButtonGroup> | ||
<Button onClick={handleRegenerate} small><Icon name='refresh' size='x16'/></Button> | ||
<Button onClick={handleRemove} small><Icon name='trash' size='x16'/></Button> | ||
</ButtonGroup> | ||
</Table.Cell> | ||
</Table.Row>; | ||
}; | ||
|
||
export function AccountTokensTable({ data, reload }) { | ||
const t = useTranslation(); | ||
const formatDateAndTime = useFormatDateAndTime(); | ||
const dispatchToastMessage = useToastMessageDispatch(); | ||
const setModal = useSetModal(); | ||
|
||
const userId = useUserId(); | ||
|
||
const regenerateToken = useMethod('personalAccessTokens:regenerateToken'); | ||
const removeToken = useMethod('personalAccessTokens:removeToken'); | ||
|
||
const [ref, isMedium] = useResizeInlineBreakpoint([600], 200); | ||
|
||
const [params, setParams] = useState({ current: 0, itemsPerPage: 25 }); | ||
|
||
const tokensTotal = data && data.success ? data.tokens.length : 0; | ||
|
||
const { current, itemsPerPage } = params; | ||
|
||
const tokens = useMemo(() => { | ||
if (!data) { return null; } | ||
if (!data.success) { return []; } | ||
const sliceStart = current > tokensTotal ? tokensTotal - itemsPerPage : current; | ||
return data.tokens.slice(sliceStart, sliceStart + itemsPerPage); | ||
}, [current, data, itemsPerPage, tokensTotal]); | ||
|
||
const closeModal = useCallback(() => setModal(null), [setModal]); | ||
|
||
const header = useMemo(() => [ | ||
<Th key={'name'}>{t('API_Personal_Access_Token_Name')}</Th>, | ||
isMedium && <Th key={'createdAt'}>{t('Created_at')}</Th>, | ||
<Th key={'lastTokenPart'}>{t('Last_token_part')}</Th>, | ||
<Th key={'2fa'}>{t('Two Factor Authentication')}</Th>, | ||
<Th key={'actions'} />, | ||
].filter(Boolean), [isMedium, t]); | ||
|
||
const onRegenerate = useCallback((name) => { | ||
const onConfirm = async () => { | ||
try { | ||
const token = await regenerateToken({ tokenName: name }); | ||
|
||
setModal(<InfoModal | ||
title={t('API_Personal_Access_Token_Generated')} | ||
content={<Box dangerouslySetInnerHTML={{ __html: t('API_Personal_Access_Token_Generated_Text_Token_s_UserId_s', { token, userId }) }}/>} | ||
confirmText={t('ok')} | ||
onConfirm={closeModal} | ||
/>); | ||
|
||
reload(); | ||
} catch (e) { | ||
dispatchToastMessage({ type: 'error', message: e }); | ||
} | ||
}; | ||
|
||
setModal(<InfoModal | ||
title={t('Are_you_sure')} | ||
content={t('API_Personal_Access_Tokens_Regenerate_Modal')} | ||
confirmText={t('API_Personal_Access_Tokens_Regenerate_It')} | ||
onConfirm={onConfirm} | ||
cancelText={t('Cancel')} | ||
onClose={closeModal} | ||
/>); | ||
}, [closeModal, dispatchToastMessage, regenerateToken, reload, setModal, t, userId]); | ||
|
||
const onRemove = useCallback((name) => { | ||
const onConfirm = async () => { | ||
try { | ||
await removeToken({ tokenName: name }); | ||
|
||
dispatchToastMessage({ type: 'success', message: t('Removed') }); | ||
reload(); | ||
closeModal(); | ||
} catch (e) { | ||
dispatchToastMessage({ type: 'error', message: e }); | ||
} | ||
}; | ||
|
||
setModal(<InfoModal | ||
title={t('Are_you_sure')} | ||
content={t('API_Personal_Access_Tokens_Remove_Modal')} | ||
confirmText={t('Yes')} | ||
onConfirm={onConfirm} | ||
cancelText={t('Cancel')} | ||
onClose={closeModal} | ||
/>); | ||
}, [closeModal, dispatchToastMessage, reload, removeToken, setModal, t]); | ||
|
||
return <GenericTable ref={ref} header={header} results={tokens} total={tokensTotal} setParams={setParams} params={params}> | ||
{useCallback((props) => <TokenRow | ||
onRegenerate={onRegenerate} | ||
onRemove={onRemove} | ||
t={t} | ||
formatDateAndTime={formatDateAndTime} | ||
isMedium={isMedium} | ||
{...props} | ||
/>, [formatDateAndTime, isMedium, onRegenerate, onRemove, t])} | ||
</GenericTable>; | ||
} | ||
|
||
export default AccountTokensTable; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import { Box, TextInput, Button, Field, FieldGroup, Margins, CheckBox } from '@rocket.chat/fuselage'; | ||
import { useUniqueId } from '@rocket.chat/fuselage-hooks'; | ||
import React, { useCallback } from 'react'; | ||
|
||
import { useSetModal } from '../../contexts/ModalContext'; | ||
import { useTranslation } from '../../contexts/TranslationContext'; | ||
import { useMethod } from '../../contexts/ServerContext'; | ||
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext'; | ||
import { useUserId } from '../../contexts/UserContext'; | ||
import { useForm } from '../../hooks/useForm'; | ||
import InfoModal from './InfoModal'; | ||
|
||
const initialValues = { | ||
name: '', | ||
bypassTwoFactor: false, | ||
}; | ||
|
||
const AddToken = ({ onDidAddToken, ...props }) => { | ||
const t = useTranslation(); | ||
const createTokenFn = useMethod('personalAccessTokens:generateToken'); | ||
const dispatchToastMessage = useToastMessageDispatch(); | ||
const setModal = useSetModal(); | ||
|
||
const userId = useUserId(); | ||
|
||
const { values, handlers, reset } = useForm(initialValues); | ||
|
||
const { name, bypassTwoFactor } = values; | ||
const { handleName, handleBypassTwoFactor } = handlers; | ||
|
||
const closeModal = useCallback(() => setModal(null), [setModal]); | ||
|
||
const handleAdd = useCallback(async () => { | ||
try { | ||
const token = await createTokenFn({ tokenName: name, bypassTwoFactor }); | ||
|
||
setModal(<InfoModal | ||
title={t('API_Personal_Access_Token_Generated')} | ||
content={<Box dangerouslySetInnerHTML={{ __html: t('API_Personal_Access_Token_Generated_Text_Token_s_UserId_s', { token, userId }) }}/>} | ||
confirmText={t('ok')} | ||
onConfirm={closeModal} | ||
/>); | ||
reset(); | ||
onDidAddToken(); | ||
} catch (error) { | ||
dispatchToastMessage({ type: 'error', message: error }); | ||
} | ||
}, [bypassTwoFactor, closeModal, createTokenFn, dispatchToastMessage, name, onDidAddToken, reset, setModal, t, userId]); | ||
|
||
const bypassTwoFactorCheckboxId = useUniqueId(); | ||
|
||
return <FieldGroup is='form' marginBlock='x8' {...props}> | ||
<Field> | ||
<Field.Row> | ||
<Margins inlineEnd='x4'> | ||
<TextInput value={name} onChange={handleName} placeholder={t('API_Add_Personal_Access_Token')}/> | ||
</Margins> | ||
<Button primary onClick={handleAdd}>{t('Add')}</Button> | ||
</Field.Row> | ||
<Field.Row> | ||
<CheckBox id={bypassTwoFactorCheckboxId} checked={bypassTwoFactor} onChange={handleBypassTwoFactor} /> | ||
<Field.Label htmlFor={bypassTwoFactorCheckboxId}>{t('Ignore')} {t('Two Factor Authentication')}</Field.Label> | ||
</Field.Row> | ||
</Field> | ||
</FieldGroup>; | ||
}; | ||
|
||
export default AddToken; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import React from 'react'; | ||
import { Button, ButtonGroup, Modal } from '@rocket.chat/fuselage'; | ||
|
||
const InfoModal = ({ title, content, icon, onConfirm, onClose, confirmText, cancelText, ...props }) => | ||
<Modal {...props}> | ||
<Modal.Header> | ||
{icon} | ||
<Modal.Title>{title}</Modal.Title> | ||
<Modal.Close onClick={onClose}/> | ||
</Modal.Header> | ||
<Modal.Content fontScale='p1'> | ||
{content} | ||
</Modal.Content> | ||
<Modal.Footer> | ||
<ButtonGroup align='end'> | ||
{cancelText && <Button onClick={onClose}>{cancelText}</Button>} | ||
{confirmText && onConfirm && <Button primary onClick={onConfirm}>{confirmText}</Button>} | ||
</ButtonGroup> | ||
</Modal.Footer> | ||
</Modal>; | ||
|
||
export default InfoModal; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters