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": [
|
||||
"./.eslintrc.js"
|
||||
]
|
||||
}
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import AdminApplication from 'pages/AdminApplication';
|
||||
import AdminTemplates from 'pages/AdminTemplates';
|
||||
import AdminCreateTemplate from 'pages/AdminCreateTemplate';
|
||||
import AdminUpdateTemplate from 'pages/AdminUpdateTemplate';
|
||||
import AdminApiTokensPage from 'pages/AdminApiTokens';
|
||||
|
||||
// TODO: consider introducing redirections to `/` as fallback
|
||||
export default (
|
||||
@@ -140,6 +141,15 @@ export default (
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={`${URLS.ADMIN_API_TOKENS}/*`}
|
||||
element={
|
||||
<Can I="manage" a="ApiToken">
|
||||
<AdminApiTokensPage />
|
||||
</Can>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={URLS.ADMIN_SETTINGS}
|
||||
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 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;
|
||||
|
||||
@@ -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_CREATE_TEMPLATE_PATTERN = `${ADMIN_SETTINGS}/templates/create/:flowId`;
|
||||
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) =>
|
||||
`/editor/create?templateId=${templateId}`;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import copy from 'clipboard-copy';
|
||||
import copyValue from './copyValue';
|
||||
|
||||
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}
|
||||
</Link>
|
||||
);
|
||||
|
||||
export const makeBold = (str) => <strong>{str}</strong>;
|
||||
|
||||
@@ -19,5 +19,9 @@ export default function userAbility(user) {
|
||||
return new PureAbility([], options);
|
||||
}
|
||||
|
||||
if (role.isAdmin) {
|
||||
return new PureAbility([{ subject: 'all', action: 'manage' }], 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.apps": "Applications",
|
||||
"adminSettingsDrawer.templates": "Templates",
|
||||
"adminSettingsDrawer.apiTokens": "API Tokens",
|
||||
"adminSettingsFooter.version": "Version {version}",
|
||||
"app.connectionCount": "{count} connections",
|
||||
"app.flowCount": "{count} flows",
|
||||
@@ -405,5 +406,21 @@
|
||||
"executionFilters.startDateLabel": "Start Date",
|
||||
"executionFilters.endDateLabel": "End Date",
|
||||
"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: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) => ({
|
||||
|
||||
Reference in New Issue
Block a user