Merge pull request #2438 from automatisch/aut-1528
feat(web): introduce API tokens in admin dashboard
This commit is contained in:
@@ -95,5 +95,6 @@
|
|||||||
"extends": [
|
"extends": [
|
||||||
"./.eslintrc.js"
|
"./.eslintrc.js"
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import AdminApplication from 'pages/AdminApplication';
|
|||||||
import AdminTemplates from 'pages/AdminTemplates';
|
import AdminTemplates from 'pages/AdminTemplates';
|
||||||
import AdminCreateTemplate from 'pages/AdminCreateTemplate';
|
import AdminCreateTemplate from 'pages/AdminCreateTemplate';
|
||||||
import AdminUpdateTemplate from 'pages/AdminUpdateTemplate';
|
import AdminUpdateTemplate from 'pages/AdminUpdateTemplate';
|
||||||
|
import AdminApiTokensPage from 'pages/AdminApiTokens';
|
||||||
|
|
||||||
// TODO: consider introducing redirections to `/` as fallback
|
// TODO: consider introducing redirections to `/` as fallback
|
||||||
export default (
|
export default (
|
||||||
@@ -140,6 +141,15 @@ export default (
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path={`${URLS.ADMIN_API_TOKENS}/*`}
|
||||||
|
element={
|
||||||
|
<Can I="manage" a="ApiToken">
|
||||||
|
<AdminApiTokensPage />
|
||||||
|
</Can>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path={URLS.ADMIN_SETTINGS}
|
path={URLS.ADMIN_SETTINGS}
|
||||||
element={<Navigate to={URLS.USERS} replace />}
|
element={<Navigate to={URLS.USERS} replace />}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import GroupsIcon from '@mui/icons-material/Groups';
|
|||||||
import LockIcon from '@mui/icons-material/LockPerson';
|
import LockIcon from '@mui/icons-material/LockPerson';
|
||||||
import BrushIcon from '@mui/icons-material/Brush';
|
import BrushIcon from '@mui/icons-material/Brush';
|
||||||
import AppsIcon from '@mui/icons-material/Apps';
|
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 { Outlet } from 'react-router-dom';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
@@ -28,6 +30,7 @@ function createDrawerLinks({
|
|||||||
canUpdateConfig,
|
canUpdateConfig,
|
||||||
canManageSamlAuthProvider,
|
canManageSamlAuthProvider,
|
||||||
canUpdateApp,
|
canUpdateApp,
|
||||||
|
canManageApiTokens,
|
||||||
}) {
|
}) {
|
||||||
const items = [
|
const items = [
|
||||||
canReadUser
|
canReadUser
|
||||||
@@ -72,12 +75,20 @@ function createDrawerLinks({
|
|||||||
: null,
|
: null,
|
||||||
canUpdateConfig
|
canUpdateConfig
|
||||||
? {
|
? {
|
||||||
Icon: AppsIcon,
|
Icon: ContentCopyIcon,
|
||||||
primary: 'adminSettingsDrawer.templates',
|
primary: 'adminSettingsDrawer.templates',
|
||||||
to: URLS.ADMIN_TEMPLATES,
|
to: URLS.ADMIN_TEMPLATES,
|
||||||
dataTest: 'templates-drawer-link',
|
dataTest: 'templates-drawer-link',
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
|
canManageApiTokens
|
||||||
|
? {
|
||||||
|
Icon: PinIcon,
|
||||||
|
primary: 'adminSettingsDrawer.apiTokens',
|
||||||
|
to: URLS.ADMIN_API_TOKENS,
|
||||||
|
dataTest: 'api-tokens-drawer-link',
|
||||||
|
}
|
||||||
|
: null,
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
@@ -102,6 +113,7 @@ function SettingsLayout() {
|
|||||||
'SamlAuthProvider',
|
'SamlAuthProvider',
|
||||||
),
|
),
|
||||||
canUpdateApp: currentUserAbility.can('manage', 'App'),
|
canUpdateApp: currentUserAbility.can('manage', 'App'),
|
||||||
|
canManageApiTokens: currentUserAbility.can('manage', 'ApiToken'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const drawerBottomLinks = [
|
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) {
|
function Can(props) {
|
||||||
const currentUserAbility = useCurrentUserAbility();
|
const currentUserAbility = useCurrentUserAbility();
|
||||||
|
|
||||||
return <OriginalCan ability={currentUserAbility} {...props} />;
|
return <OriginalCan ability={currentUserAbility} {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ function ConditionalIconButton(props) {
|
|||||||
size={buttonProps.size}
|
size={buttonProps.size}
|
||||||
component={buttonProps.component}
|
component={buttonProps.component}
|
||||||
to={buttonProps.to}
|
to={buttonProps.to}
|
||||||
|
onClick={buttonProps.onClick}
|
||||||
disabled={buttonProps.disabled}
|
disabled={buttonProps.disabled}
|
||||||
data-test={buttonProps['data-test']}
|
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 Card from '@mui/material/Card';
|
||||||
import AddCircleIcon from '@mui/icons-material/AddCircle';
|
import AddCircleIcon from '@mui/icons-material/AddCircle';
|
||||||
import CardActionArea from '@mui/material/CardActionArea';
|
import CardActionArea from '@mui/material/CardActionArea';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { CardContent } from './style';
|
import { CardContent } from './style';
|
||||||
|
|
||||||
function NoResultFound(props) {
|
function NoResultFound(props) {
|
||||||
const { text, to } = props;
|
const { onClick, text, to } = props;
|
||||||
|
|
||||||
const ActionAreaLink = React.useMemo(
|
const ActionAreaLink = React.useMemo(
|
||||||
() =>
|
() =>
|
||||||
React.forwardRef(function InlineLink(linkProps, ref) {
|
React.forwardRef(function InlineLink(linkProps, ref) {
|
||||||
if (!to) return <div>{linkProps.children}</div>;
|
if (to) return <Link ref={ref} to={to} {...linkProps} />;
|
||||||
return <Link ref={ref} to={to} {...linkProps} />;
|
|
||||||
|
if (onClick) return <Button onClick={onClick} {...linkProps} />;
|
||||||
|
|
||||||
|
return <div>{linkProps.children}</div>;
|
||||||
}),
|
}),
|
||||||
[to],
|
[to, onClick],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card elevation={0} data-test="no-results">
|
<Card elevation={0} data-test="no-results">
|
||||||
<CardActionArea component={ActionAreaLink} {...props}>
|
<CardActionArea component={ActionAreaLink} {...props}>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{!!to && <AddCircleIcon color="primary" />}
|
{(!!to || !!onClick) && <AddCircleIcon color="primary" />}
|
||||||
<Typography variant="body1">{text}</Typography>
|
<Typography variant="body1">{text}</Typography>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</CardActionArea>
|
</CardActionArea>
|
||||||
@@ -35,6 +39,7 @@ function NoResultFound(props) {
|
|||||||
NoResultFound.propTypes = {
|
NoResultFound.propTypes = {
|
||||||
text: PropTypes.string,
|
text: PropTypes.string,
|
||||||
to: PropTypes.string,
|
to: PropTypes.string,
|
||||||
|
onClick: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NoResultFound;
|
export default NoResultFound;
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ export const ADMIN_APP_CONNECTIONS_PATTERN = `${ADMIN_SETTINGS}/apps/:appKey/con
|
|||||||
export const ADMIN_TEMPLATES = `${ADMIN_SETTINGS}/templates`;
|
export const ADMIN_TEMPLATES = `${ADMIN_SETTINGS}/templates`;
|
||||||
export const ADMIN_CREATE_TEMPLATE_PATTERN = `${ADMIN_SETTINGS}/templates/create/:flowId`;
|
export const ADMIN_CREATE_TEMPLATE_PATTERN = `${ADMIN_SETTINGS}/templates/create/:flowId`;
|
||||||
export const ADMIN_UPDATE_TEMPLATE_PATTERN = `${ADMIN_SETTINGS}/templates/update/:templateId`;
|
export const ADMIN_UPDATE_TEMPLATE_PATTERN = `${ADMIN_SETTINGS}/templates/update/:templateId`;
|
||||||
|
export const ADMIN_API_TOKENS = `${ADMIN_SETTINGS}/api-tokens`;
|
||||||
|
|
||||||
export const CREATE_FLOW_FROM_TEMPLATE = (templateId) =>
|
export const CREATE_FLOW_FROM_TEMPLATE = (templateId) =>
|
||||||
`/editor/create?templateId=${templateId}`;
|
`/editor/create?templateId=${templateId}`;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import copy from 'clipboard-copy';
|
import copyValue from './copyValue';
|
||||||
|
|
||||||
export default function copyInputValue(element) {
|
export default function copyInputValue(element) {
|
||||||
copy(element.value);
|
copyValue(element.value);
|
||||||
}
|
}
|
||||||
|
|||||||
5
packages/web/src/helpers/copyValue.js
Normal file
5
packages/web/src/helpers/copyValue.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import copy from 'clipboard-copy';
|
||||||
|
|
||||||
|
export default function copyInputValue(value) {
|
||||||
|
copy(value);
|
||||||
|
}
|
||||||
@@ -12,3 +12,5 @@ export const generateExternalLink = (link) => (str) => (
|
|||||||
{str}
|
{str}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const makeBold = (str) => <strong>{str}</strong>;
|
||||||
|
|||||||
@@ -19,5 +19,9 @@ export default function userAbility(user) {
|
|||||||
return new PureAbility([], options);
|
return new PureAbility([], options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (role.isAdmin) {
|
||||||
|
return new PureAbility([{ subject: 'all', action: 'manage' }], options);
|
||||||
|
}
|
||||||
|
|
||||||
return new PureAbility(permissions, options);
|
return new PureAbility(permissions, options);
|
||||||
}
|
}
|
||||||
|
|||||||
17
packages/web/src/hooks/useAdminApiTokens.ee.js
Normal file
17
packages/web/src/hooks/useAdminApiTokens.ee.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import api from 'helpers/api';
|
||||||
|
|
||||||
|
export default function useAdminApiTokens() {
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: ['admin', 'apiTokens'],
|
||||||
|
queryFn: async ({ signal }) => {
|
||||||
|
const { data } = await api.get(`/v1/admin/api-tokens`, {
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
21
packages/web/src/hooks/useAdminCreateApiToken.ee.js
Normal file
21
packages/web/src/hooks/useAdminCreateApiToken.ee.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import api from 'helpers/api';
|
||||||
|
|
||||||
|
export default function useAdminCreateApiToken() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
const { data } = await api.post('/v1/admin/api-tokens');
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: async () => {
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: ['admin', 'apiTokens'],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return mutation;
|
||||||
|
}
|
||||||
22
packages/web/src/hooks/useAdminDeleteApiToken.ee.js
Normal file
22
packages/web/src/hooks/useAdminDeleteApiToken.ee.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import api from 'helpers/api';
|
||||||
|
|
||||||
|
export default function useAdminDeleteApiToken(apiTokenid) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
const { data } = await api.delete(`/v1/admin/api-tokens/${apiTokenid}`);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: async () => {
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: ['admin', 'apiTokens'],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return mutation;
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
"adminSettingsDrawer.goBack": "Go to the dashboard",
|
"adminSettingsDrawer.goBack": "Go to the dashboard",
|
||||||
"adminSettingsDrawer.apps": "Applications",
|
"adminSettingsDrawer.apps": "Applications",
|
||||||
"adminSettingsDrawer.templates": "Templates",
|
"adminSettingsDrawer.templates": "Templates",
|
||||||
|
"adminSettingsDrawer.apiTokens": "API Tokens",
|
||||||
"adminSettingsFooter.version": "Version {version}",
|
"adminSettingsFooter.version": "Version {version}",
|
||||||
"app.connectionCount": "{count} connections",
|
"app.connectionCount": "{count} connections",
|
||||||
"app.flowCount": "{count} flows",
|
"app.flowCount": "{count} flows",
|
||||||
@@ -405,5 +406,21 @@
|
|||||||
"executionFilters.startDateLabel": "Start Date",
|
"executionFilters.startDateLabel": "Start Date",
|
||||||
"executionFilters.endDateLabel": "End Date",
|
"executionFilters.endDateLabel": "End Date",
|
||||||
"permissionCatalogField.ownEntitiesLabel": "(own entities)",
|
"permissionCatalogField.ownEntitiesLabel": "(own entities)",
|
||||||
"permissionCatalogField.allEntitiesLabel": "(all entities)"
|
"permissionCatalogField.allEntitiesLabel": "(all entities)",
|
||||||
|
"adminApiTokensPage.title": "API Tokens",
|
||||||
|
"adminApiTokensPage.createApiToken": "Create API Token",
|
||||||
|
"adminApiTokenList.token": "Token",
|
||||||
|
"adminApiTokenList.createdAt": "Created",
|
||||||
|
"adminApiTokensPage.noApiTokens": "You don't have any API tokens yet.",
|
||||||
|
"deleteApiTokenButton.deleteError": "An error occurred while deleting the API token.",
|
||||||
|
"deleteApiTokenButton.successfullyDeleted": "The API token has been successfully deleted.",
|
||||||
|
"deleteApiTokenButton.title": "Delete API Token",
|
||||||
|
"deleteApiTokenButton.description": "Are you sure you want to delete the API token?",
|
||||||
|
"deleteApiTokenButton.cancel": "Cancel",
|
||||||
|
"deleteApiTokenButton.confirm": "Confirm",
|
||||||
|
"createdApiTokenDialog.title": "Your API Token",
|
||||||
|
"createdApiTokenDialog.description": "Here is your API Token. Keep it secure, as anyone with your API key can make authenticated requests with it.",
|
||||||
|
"createdApiTokenDialog.apiTokenFieldLabel": "API Token",
|
||||||
|
"createdApiTokenDialog.warningForApiToken": "Please save your API token in a safe place since <strong>you won't be able to view it again</strong>.",
|
||||||
|
"createdApiTokenDialog.close": "Close"
|
||||||
}
|
}
|
||||||
|
|||||||
96
packages/web/src/pages/AdminApiTokens/index.jsx
Normal file
96
packages/web/src/pages/AdminApiTokens/index.jsx
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import Grid from '@mui/material/Grid';
|
||||||
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
|
import PageTitle from 'components/PageTitle';
|
||||||
|
import Container from 'components/Container';
|
||||||
|
import ApiTokenList from 'components/ApiTokenList';
|
||||||
|
import ConditionalIconButton from 'components/ConditionalIconButton';
|
||||||
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
import useAdminCreateApiToken from 'hooks/useAdminCreateApiToken.ee';
|
||||||
|
import CreatedApiTokenDialog from 'components/CreatedApiTokenDialog';
|
||||||
|
import useAdminApiTokens from 'hooks/useAdminApiTokens.ee';
|
||||||
|
import NoResultFound from 'components/NoResultFound';
|
||||||
|
|
||||||
|
function AdminApiTokensPage() {
|
||||||
|
const formatMessage = useFormatMessage();
|
||||||
|
const { data: apiTokensData, isLoading } = useAdminApiTokens();
|
||||||
|
const {
|
||||||
|
mutate: createApiToken,
|
||||||
|
reset,
|
||||||
|
data: createdApiTokenData,
|
||||||
|
isPending,
|
||||||
|
} = useAdminCreateApiToken();
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
|
||||||
|
const apiTokens = apiTokensData?.data;
|
||||||
|
const createdApiToken = createdApiTokenData?.data;
|
||||||
|
|
||||||
|
const onCreateApiToken = async () => {
|
||||||
|
await createApiToken();
|
||||||
|
|
||||||
|
setOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDialogClose = () => {
|
||||||
|
reset();
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CreatedApiTokenDialog
|
||||||
|
open={open}
|
||||||
|
onClose={onDialogClose}
|
||||||
|
apiToken={createdApiToken?.token}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
|
||||||
|
<Grid container item xs={12} sm={10} md={9}>
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
sx={{ mb: [0, 3] }}
|
||||||
|
columnSpacing={1.5}
|
||||||
|
rowSpacing={3}
|
||||||
|
>
|
||||||
|
<Grid container item xs sm alignItems="center">
|
||||||
|
<PageTitle data-test="admin-api-tokens-page-title">
|
||||||
|
{formatMessage('adminApiTokensPage.title')}
|
||||||
|
</PageTitle>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid container item xs="auto" sm="auto" alignItems="center">
|
||||||
|
<ConditionalIconButton
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
size="large"
|
||||||
|
onClick={onCreateApiToken}
|
||||||
|
disabled={isPending}
|
||||||
|
fullWidth
|
||||||
|
icon={<AddIcon />}
|
||||||
|
data-test="create-user"
|
||||||
|
>
|
||||||
|
{formatMessage('adminApiTokensPage.createApiToken')}
|
||||||
|
</ConditionalIconButton>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}>
|
||||||
|
{!isLoading && apiTokensData?.meta.count === 0 && (
|
||||||
|
<NoResultFound
|
||||||
|
onClick={onCreateApiToken}
|
||||||
|
text={formatMessage('adminApiTokensPage.noApiTokens')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(isLoading || apiTokensData?.meta.count > 0) && (
|
||||||
|
<ApiTokenList apiTokens={apiTokens} loading={isLoading} />
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Container>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AdminApiTokensPage;
|
||||||
@@ -276,6 +276,15 @@ export const defaultTheme = createTheme({
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
MuiDialogActions: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: ({ theme }) => ({
|
||||||
|
'&&': {
|
||||||
|
paddingRight: theme.spacing(3),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
MuiDialogTitle: {
|
MuiDialogTitle: {
|
||||||
styleOverrides: {
|
styleOverrides: {
|
||||||
root: ({ theme }) => ({
|
root: ({ theme }) => ({
|
||||||
|
|||||||
Reference in New Issue
Block a user