feat(web): introduce API tokens in admin dashboard
This commit is contained in:
@@ -4,6 +4,8 @@ import GroupsIcon from '@mui/icons-material/Groups';
|
||||
import LockIcon from '@mui/icons-material/LockPerson';
|
||||
import BrushIcon from '@mui/icons-material/Brush';
|
||||
import AppsIcon from '@mui/icons-material/Apps';
|
||||
import PinIcon from '@mui/icons-material/Pin';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import Box from '@mui/material/Box';
|
||||
import Stack from '@mui/material/Stack';
|
||||
@@ -28,6 +30,7 @@ function createDrawerLinks({
|
||||
canUpdateConfig,
|
||||
canManageSamlAuthProvider,
|
||||
canUpdateApp,
|
||||
canManageApiTokens,
|
||||
}) {
|
||||
const items = [
|
||||
canReadUser
|
||||
@@ -72,12 +75,20 @@ function createDrawerLinks({
|
||||
: null,
|
||||
canUpdateConfig
|
||||
? {
|
||||
Icon: AppsIcon,
|
||||
Icon: ContentCopyIcon,
|
||||
primary: 'adminSettingsDrawer.templates',
|
||||
to: URLS.ADMIN_TEMPLATES,
|
||||
dataTest: 'templates-drawer-link',
|
||||
}
|
||||
: null,
|
||||
canManageApiTokens
|
||||
? {
|
||||
Icon: PinIcon,
|
||||
primary: 'adminSettingsDrawer.apiTokens',
|
||||
to: URLS.ADMIN_API_TOKENS,
|
||||
dataTest: 'api-tokens-drawer-link',
|
||||
}
|
||||
: null,
|
||||
].filter(Boolean);
|
||||
|
||||
return items;
|
||||
@@ -102,6 +113,7 @@ function SettingsLayout() {
|
||||
'SamlAuthProvider',
|
||||
),
|
||||
canUpdateApp: currentUserAbility.can('manage', 'App'),
|
||||
canManageApiTokens: currentUserAbility.can('manage', 'ApiToken'),
|
||||
});
|
||||
|
||||
const drawerBottomLinks = [
|
||||
|
||||
100
packages/web/src/components/ApiTokenList/index.jsx
Normal file
100
packages/web/src/components/ApiTokenList/index.jsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import TableHead from '@mui/material/TableHead';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { DateTime } from 'luxon';
|
||||
import * as React from 'react';
|
||||
|
||||
import ListLoader from 'components/ListLoader';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import DeleteApiTokenButton from 'components/DeleteApiTokenButton/index.ee';
|
||||
|
||||
export default function ApiTokenList({ loading, apiTokens }) {
|
||||
const formatMessage = useFormatMessage();
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell component="th">
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{ color: 'text.secondary', fontWeight: 700 }}
|
||||
>
|
||||
{formatMessage('adminApiTokenList.token')}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell component="th">
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{ color: 'text.secondary', fontWeight: 700 }}
|
||||
>
|
||||
{formatMessage('adminApiTokenList.createdAt')}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell component="th" />
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{loading && (
|
||||
<ListLoader
|
||||
data-test="apiTokens-list-loader"
|
||||
rowsNumber={3}
|
||||
columnsNumber={3}
|
||||
/>
|
||||
)}
|
||||
{!loading &&
|
||||
apiTokens.map((apiToken) => (
|
||||
<TableRow
|
||||
key={apiToken.id}
|
||||
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
|
||||
data-test="api-token-row"
|
||||
>
|
||||
<TableCell scope="row">
|
||||
<Typography variant="subtitle2" data-test="api-token-token">
|
||||
{apiToken.token}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
data-test="api-token-created-at"
|
||||
>
|
||||
{DateTime.fromMillis(
|
||||
parseInt(apiToken.createdAt, 10),
|
||||
).toLocaleString(DateTime.DATETIME_MED)}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<Stack direction="row" gap={1} justifyContent="right">
|
||||
<DeleteApiTokenButton
|
||||
data-test="api-token-delete"
|
||||
apiTokenId={apiToken.id}
|
||||
/>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ApiTokenList.propTypes = {
|
||||
apiTokens: PropTypes.array,
|
||||
loading: PropTypes.bool,
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import useCurrentUserAbility from 'hooks/useCurrentUserAbility';
|
||||
|
||||
function Can(props) {
|
||||
const currentUserAbility = useCurrentUserAbility();
|
||||
|
||||
return <OriginalCan ability={currentUserAbility} {...props} />;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ function ConditionalIconButton(props) {
|
||||
size={buttonProps.size}
|
||||
component={buttonProps.component}
|
||||
to={buttonProps.to}
|
||||
onClick={buttonProps.onClick}
|
||||
disabled={buttonProps.disabled}
|
||||
data-test={buttonProps['data-test']}
|
||||
>
|
||||
|
||||
86
packages/web/src/components/CreatedApiTokenDialog/index.jsx
Normal file
86
packages/web/src/components/CreatedApiTokenDialog/index.jsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Button from '@mui/material/Button';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
import MuiTextField from '@mui/material/TextField';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as React from 'react';
|
||||
|
||||
import copyValue from 'helpers/copyValue';
|
||||
import { makeBold } from 'helpers/translationValues';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
|
||||
function CreatedApiTokenDialog(props) {
|
||||
const {
|
||||
open = true,
|
||||
'data-test': dataTest = 'created-api-token-dialog',
|
||||
onClose,
|
||||
apiToken,
|
||||
} = props;
|
||||
|
||||
const formatMessage = useFormatMessage();
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} data-test={dataTest}>
|
||||
<DialogTitle>{formatMessage('createdApiTokenDialog.title')}</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<DialogContentText sx={{ mb: 4 }}>
|
||||
{formatMessage('createdApiTokenDialog.description')}
|
||||
</DialogContentText>
|
||||
|
||||
<MuiTextField
|
||||
label={formatMessage('createdApiTokenDialog.apiTokenFieldLabel')}
|
||||
variant="outlined"
|
||||
value={apiToken}
|
||||
fullWidth
|
||||
InputLabelProps={{ shrink: true }}
|
||||
InputProps={{
|
||||
readOnly: true,
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={() => copyValue(apiToken)} edge="end">
|
||||
<ContentCopyIcon color="primary" />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
inputProps={{
|
||||
'data-test': 'api-token-field',
|
||||
}}
|
||||
/>
|
||||
|
||||
<Alert severity="error" sx={{ mt: 1 }}>
|
||||
{formatMessage('createdApiTokenDialog.warningForApiToken', {
|
||||
strong: makeBold,
|
||||
})}
|
||||
</Alert>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions sx={{ mb: 1 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={onClose}
|
||||
data-test="import-flow-dialog-close-button"
|
||||
>
|
||||
{formatMessage('createdApiTokenDialog.close')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
CreatedApiTokenDialog.propTypes = {
|
||||
open: PropTypes.bool,
|
||||
'data-test': PropTypes.string,
|
||||
apiToken: PropTypes.string,
|
||||
onClose: PropTypes.func,
|
||||
};
|
||||
|
||||
export default CreatedApiTokenDialog;
|
||||
@@ -0,0 +1,79 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
|
||||
import { getGeneralErrorMessage } from 'helpers/errors';
|
||||
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
||||
import * as React from 'react';
|
||||
import ConfirmationDialog from 'components/ConfirmationDialog';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import useAdminDeleteApiToken from 'hooks/useAdminDeleteApiToken.ee';
|
||||
|
||||
function DeleteApiTokenButton(props) {
|
||||
const { apiTokenId } = props;
|
||||
const [showConfirmation, setShowConfirmation] = React.useState(false);
|
||||
const {
|
||||
mutateAsync: deleteApiToken,
|
||||
error: deleteApiTokenError,
|
||||
reset: resetDeleteApiToken,
|
||||
} = useAdminDeleteApiToken(apiTokenId);
|
||||
|
||||
const formatMessage = useFormatMessage();
|
||||
const enqueueSnackbar = useEnqueueSnackbar();
|
||||
|
||||
const generalErrorMessage = getGeneralErrorMessage({
|
||||
error: deleteApiTokenError,
|
||||
fallbackMessage: formatMessage('deleteApiTokenButton.deleteError'),
|
||||
});
|
||||
|
||||
const handleConfirm = React.useCallback(async () => {
|
||||
try {
|
||||
await deleteApiToken();
|
||||
setShowConfirmation(false);
|
||||
enqueueSnackbar(
|
||||
formatMessage('deleteApiTokenButton.successfullyDeleted'),
|
||||
{
|
||||
variant: 'success',
|
||||
SnackbarProps: {
|
||||
'data-test': 'snackbar-delete-api-token-success',
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch {}
|
||||
}, [deleteApiToken]);
|
||||
|
||||
const handleClose = () => {
|
||||
setShowConfirmation(false);
|
||||
resetDeleteApiToken();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
data-test="delete-button"
|
||||
onClick={() => setShowConfirmation(true)}
|
||||
size="small"
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
|
||||
<ConfirmationDialog
|
||||
open={showConfirmation}
|
||||
title={formatMessage('deleteApiTokenButton.title')}
|
||||
description={formatMessage('deleteApiTokenButton.description')}
|
||||
onClose={handleClose}
|
||||
onConfirm={handleConfirm}
|
||||
cancelButtonChildren={formatMessage('deleteApiTokenButton.cancel')}
|
||||
confirmButtonChildren={formatMessage('deleteApiTokenButton.confirm')}
|
||||
data-test="delete-api-token-modal"
|
||||
errorMessage={generalErrorMessage}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
DeleteApiTokenButton.propTypes = {
|
||||
apiTokenId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default DeleteApiTokenButton;
|
||||
@@ -3,28 +3,32 @@ import { Link } from 'react-router-dom';
|
||||
import Card from '@mui/material/Card';
|
||||
import AddCircleIcon from '@mui/icons-material/AddCircle';
|
||||
import CardActionArea from '@mui/material/CardActionArea';
|
||||
import Button from '@mui/material/Button';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { CardContent } from './style';
|
||||
|
||||
function NoResultFound(props) {
|
||||
const { text, to } = props;
|
||||
const { onClick, text, to } = props;
|
||||
|
||||
const ActionAreaLink = React.useMemo(
|
||||
() =>
|
||||
React.forwardRef(function InlineLink(linkProps, ref) {
|
||||
if (!to) return <div>{linkProps.children}</div>;
|
||||
return <Link ref={ref} to={to} {...linkProps} />;
|
||||
if (to) return <Link ref={ref} to={to} {...linkProps} />;
|
||||
|
||||
if (onClick) return <Button onClick={onClick} {...linkProps} />;
|
||||
|
||||
return <div>{linkProps.children}</div>;
|
||||
}),
|
||||
[to],
|
||||
[to, onClick],
|
||||
);
|
||||
|
||||
return (
|
||||
<Card elevation={0} data-test="no-results">
|
||||
<CardActionArea component={ActionAreaLink} {...props}>
|
||||
<CardContent>
|
||||
{!!to && <AddCircleIcon color="primary" />}
|
||||
{(!!to || !!onClick) && <AddCircleIcon color="primary" />}
|
||||
<Typography variant="body1">{text}</Typography>
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
@@ -35,6 +39,7 @@ function NoResultFound(props) {
|
||||
NoResultFound.propTypes = {
|
||||
text: PropTypes.string,
|
||||
to: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default NoResultFound;
|
||||
|
||||
Reference in New Issue
Block a user