@craftercms/studio-ui
Version:
Services, components, models & utils to build CrafterCMS authoring extensions.
579 lines (577 loc) • 21.9 kB
JavaScript
/*
* Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License version 3 as published by
* the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as published by
* the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useEffect, useMemo, useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { darken, lighten } from '@mui/material/styles';
import { makeStyles, withStyles } from 'tss-react/mui';
import AddIcon from '@mui/icons-material/Add';
import DeleteIcon from '@mui/icons-material/DeleteOutline';
import { AsDayMonthDateTime } from '../VersionList';
import { deleteToken, fetchTokens as fetchTokensService, updateToken } from '../../services/tokens';
import { useDispatch } from 'react-redux';
import CreateTokenDialog from '../CreateTokenDialog';
import { showSystemNotification } from '../../state/actions/system';
import ConfirmDropdown from '../ConfirmDropdown';
import ActionsBar from '../ActionsBar';
import { ConditionalLoadingState } from '../LoadingState/LoadingState';
import EmptyState from '../EmptyState/EmptyState';
import CopyTokenDialog from '../CopyTokenDialog/CopyTokenDialog';
import moment from 'moment-timezone';
import { forkJoin } from 'rxjs';
import Typography from '@mui/material/Typography';
import TableContainer from '@mui/material/TableContainer';
import Table from '@mui/material/Table';
import TableRow from '@mui/material/TableRow';
import Checkbox from '@mui/material/Checkbox';
import TableCell from '@mui/material/TableCell';
import TableHead from '@mui/material/TableHead';
import TableBody from '@mui/material/TableBody';
import Chip from '@mui/material/Chip';
import Switch from '@mui/material/Switch';
import { showErrorDialog } from '../../state/reducers/dialogs/error';
import GlobalAppToolbar from '../GlobalAppToolbar';
import Button from '@mui/material/Button';
import Paper from '@mui/material/Paper';
import { useEnhancedDialogState } from '../../hooks/useEnhancedDialogState';
import { useWithPendingChangesCloseRequest } from '../../hooks/useWithPendingChangesCloseRequest';
import Tooltip from '@mui/material/Tooltip';
import { CreatePreviewTokenDialog } from '../CreatePreviewTokenDialog';
const styles = makeStyles()((theme) => ({
table: {
minWidth: 650
},
actions: {
width: '150px',
padding: '5px 20px'
},
actionsBar: {
position: 'absolute',
left: 0,
right: 0,
zIndex: 1
},
chip: {
backgroundColor:
theme.palette.mode === 'light'
? lighten(theme.palette.success.main, 0.9)
: darken(theme.palette.success.main, 0.9),
height: 'auto',
padding: '4px 6.5px',
'&.disabled': {
backgroundColor:
theme.palette.mode === 'light'
? lighten(theme.palette.warning.main, 0.9)
: darken(theme.palette.warning.main, 0.9)
},
'&.expired': {
backgroundColor:
theme.palette.mode === 'light' ? lighten(theme.palette.error.main, 0.9) : darken(theme.palette.error.main, 0.9)
}
}
}));
const StyledTableCell = withStyles(TableCell, () => ({
root: {
padding: '5px'
}
}));
const translations = defineMessages({
tokenCreated: {
id: 'tokenManagement.created',
defaultMessage: 'Token created and copied to clipboard'
},
tokenCopied: {
id: 'tokenManagement.copied',
defaultMessage: 'Token copied to clipboard'
},
tokenDeleted: {
id: 'tokenManagement.deleted',
defaultMessage: '{count, plural, one {Token deleted} other {The selected tokens were deleted}}'
},
tokenUpdated: {
id: 'tokenManagement.updated',
defaultMessage: 'Token updated'
},
never: {
id: 'words.never',
defaultMessage: 'Never'
},
confirmHelperText: {
id: 'tokenManagement.helperText',
defaultMessage: 'Delete "{label}" token?'
},
confirmOk: {
id: 'words.yes',
defaultMessage: 'Yes'
},
confirmCancel: {
id: 'words.no',
defaultMessage: 'No'
},
deletedSelected: {
id: 'tokenManagement.deletedSelected',
defaultMessage: 'Delete Selected'
},
clearSelected: {
id: 'tokenManagement.clearSelected',
defaultMessage: 'Clear Selected ({count})'
}
});
export function TokenManagement() {
const { classes, cx } = styles();
const dispatch = useDispatch();
const { formatMessage } = useIntl();
const [tokens, setTokens] = useState(null);
const [checkedLookup, setCheckedLookup] = useState({});
const [createdToken, setCreatedToken] = useState(null);
const checkedCount = useMemo(() => Object.values(checkedLookup).filter(Boolean).length, [checkedLookup]);
const options = useMemo(
() => [
{
id: 'delete',
label: formatMessage(translations.deletedSelected)
},
{
id: 'clear',
label: formatMessage(translations.clearSelected, { count: checkedCount })
}
],
[checkedCount, formatMessage]
);
const fetchTokens = () => {
fetchTokensService().subscribe((tokens) => {
setTokens(tokens);
});
};
useEffect(() => {
fetchTokens();
}, []);
const createTokenDialogState = useEnhancedDialogState();
const createPreviewTokenDialogState = useEnhancedDialogState();
const createTokenDialogPendingChangesCloseRequest = useWithPendingChangesCloseRequest(createTokenDialogState.onClose);
const createPreviewTokenDialogPendingChangesCloseRequest = useWithPendingChangesCloseRequest(
createPreviewTokenDialogState.onClose
);
const copyTokenDialogState = useEnhancedDialogState();
const copyTokenDialogPendingChangesCloseRequest = useWithPendingChangesCloseRequest(copyTokenDialogState.onClose);
const onCreateToken = () => {
createTokenDialogState.onOpen();
};
const handleCreatePreviewToken = () => {
createPreviewTokenDialogState.onOpen();
};
const onCopyTokenDialogClosed = () => {
copyTokenDialogState.onClose();
setTimeout(() => {
setCreatedToken(null);
});
};
const onTokenCreated = (token) => {
fetchTokens();
dispatch(
showSystemNotification({
message: formatMessage(translations.tokenCreated)
})
);
createTokenDialogState.onClose();
copyTokenDialogState.onOpen();
setCreatedToken(token);
};
const onTokenCopied = () => {
dispatch(
showSystemNotification({
message: formatMessage(translations.tokenCopied)
})
);
};
const onSetEnabled = (id, checked) => {
const _tokens = { ...tokens };
setTokens(
tokens.map((token) => {
if (token.id === id) {
return { ...token, enabled: checked };
} else {
return token;
}
})
);
updateToken(id, {
enabled: checked
}).subscribe(
(token) => {
fetchTokens();
dispatch(
showSystemNotification({
message: formatMessage(translations.tokenUpdated)
})
);
},
(response) => {
setTokens(_tokens);
dispatch(showErrorDialog({ error: response }));
}
);
};
const onDeleteToken = (id) => {
const _tokens = { ...tokens };
setTokens(tokens.filter((token) => token.id !== id));
setCheckedLookup({
...checkedLookup,
[id]: false
});
deleteToken(id).subscribe(
(token) => {
fetchTokens();
dispatch(
showSystemNotification({
message: formatMessage(translations.tokenDeleted, { count: 1 })
})
);
},
(response) => {
setTokens(_tokens);
setCheckedLookup({
...checkedLookup,
[id]: false
});
dispatch(showErrorDialog({ error: response }));
}
);
};
const onOptionClicked = (action) => {
switch (action) {
case 'delete': {
const requests = [];
let checkedIds = [];
let _checkedLookup = { ...checkedLookup };
Object.keys(checkedLookup).forEach((id, i) => {
if (checkedLookup[id]) {
checkedIds.push(parseInt(id));
_checkedLookup[id] = false;
requests.push(deleteToken(parseInt(id)));
}
});
setCheckedLookup(_checkedLookup);
setTokens(tokens.filter((token) => !checkedIds.includes(token.id)));
forkJoin(requests).subscribe(
(responses) => {
dispatch(
showSystemNotification({
message: formatMessage(translations.tokenDeleted, { count: checkedIds.length })
})
);
},
(response) => {
fetchTokens();
dispatch(showErrorDialog({ error: response }));
}
);
break;
}
case 'clear': {
onToggleSelectAll(false);
break;
}
}
};
const onItemChecked = (id, checked) => {
setCheckedLookup({ ...checkedLookup, [id]: checked });
};
const onToggleSelectAll = (check) => {
const _checkedLookup = {};
tokens.forEach((token) => {
_checkedLookup[token.id] = check;
});
setCheckedLookup(_checkedLookup);
};
return React.createElement(
Paper,
{ elevation: 0 },
React.createElement(GlobalAppToolbar, {
title: React.createElement(FormattedMessage, {
id: 'globalMenu.tokenManagement',
defaultMessage: 'Token Management'
}),
leftContent: React.createElement(
React.Fragment,
null,
React.createElement(
Button,
{
startIcon: React.createElement(AddIcon, null),
variant: 'outlined',
color: 'primary',
onClick: onCreateToken,
sx: { mr: 1 },
title: formatMessage({ defaultMessage: 'Create API Token' })
},
React.createElement(FormattedMessage, { defaultMessage: 'API Token' })
),
React.createElement(
Button,
{
startIcon: React.createElement(AddIcon, null),
variant: 'outlined',
color: 'primary',
onClick: handleCreatePreviewToken,
title: formatMessage({ defaultMessage: 'Create Preview Token' })
},
React.createElement(FormattedMessage, { defaultMessage: 'Preview Token' })
)
)
}),
React.createElement(
ConditionalLoadingState,
{ isLoading: tokens === null },
tokens?.length
? React.createElement(
React.Fragment,
null,
React.createElement(
TableContainer,
{ style: { position: 'relative' } },
checkedCount > 0 &&
React.createElement(ActionsBar, {
onOptionClicked: onOptionClicked,
options: options,
isIndeterminate: checkedCount > 0 && checkedCount < tokens.length,
isChecked: checkedCount === tokens.length,
onCheckboxChange: () => onToggleSelectAll(checkedCount !== tokens.length),
classes: {
root: classes.actionsBar
}
}),
React.createElement(
Table,
{ className: classes.table },
React.createElement(
TableHead,
null,
React.createElement(
TableRow,
null,
React.createElement(
TableCell,
{ padding: 'checkbox' },
React.createElement(Checkbox, {
disabled: checkedCount > 0,
checked: false,
color: 'primary',
onClick: () => {
onToggleSelectAll(true);
}
})
),
React.createElement(
TableCell,
{ align: 'left', padding: 'none' },
React.createElement(
Typography,
{ variant: 'subtitle2' },
React.createElement(FormattedMessage, { id: 'words.status', defaultMessage: 'Status' })
)
),
React.createElement(
StyledTableCell,
{ align: 'left' },
React.createElement(
Typography,
{ variant: 'subtitle2' },
React.createElement(FormattedMessage, { id: 'words.label', defaultMessage: 'Label' })
)
),
React.createElement(
StyledTableCell,
{ align: 'left' },
React.createElement(
Typography,
{ variant: 'subtitle2' },
React.createElement(FormattedMessage, { id: 'words.expiration', defaultMessage: 'Expiration' })
)
),
React.createElement(
StyledTableCell,
{ align: 'left' },
React.createElement(
Typography,
{ variant: 'subtitle2' },
React.createElement(FormattedMessage, { id: 'words.created', defaultMessage: 'Created' })
)
),
React.createElement(TableCell, { align: 'center', className: classes.actions })
)
),
React.createElement(
TableBody,
null,
tokens.map((token) =>
React.createElement(
TableRow,
{ key: token.id },
React.createElement(
TableCell,
{ padding: 'checkbox' },
React.createElement(Checkbox, {
checked: !!checkedLookup[token.id],
color: 'primary',
onChange: (event, checked) => {
onItemChecked(token.id, checked);
}
})
),
React.createElement(
TableCell,
{ component: 'th', id: token.id.toString(), scope: 'row', padding: 'none' },
React.createElement(Chip, {
label:
Date.parse(token.expiresAt) < Date.now()
? React.createElement(FormattedMessage, {
id: 'words.expired',
defaultMessage: 'Expired'
})
: token.enabled
? React.createElement(FormattedMessage, {
id: 'words.enabled',
defaultMessage: 'Enabled'
})
: React.createElement(FormattedMessage, {
id: 'words.disabled',
defaultMessage: 'Disabled'
}),
className: cx(
classes.chip,
!token.enabled && 'disabled',
moment(token.expiresAt) < moment() && 'expired'
)
})
),
React.createElement(StyledTableCell, { align: 'left' }, token.label),
React.createElement(
StyledTableCell,
{ align: 'left' },
token.expiresAt
? React.createElement(AsDayMonthDateTime, { date: token.expiresAt })
: React.createElement(
Typography,
{ color: 'textSecondary', variant: 'body2' },
'(',
formatMessage(translations.never).toLowerCase(),
')'
)
),
React.createElement(
StyledTableCell,
{ align: 'left' },
React.createElement(AsDayMonthDateTime, { date: token.createdOn })
),
React.createElement(
TableCell,
{ align: 'right', className: classes.actions },
(token.expiresAt === null || moment(token.expiresAt) > moment()) &&
React.createElement(
Tooltip,
{
title: token.enabled
? React.createElement(FormattedMessage, { defaultMessage: 'Disable' })
: React.createElement(FormattedMessage, { defaultMessage: 'Enable' })
},
React.createElement(Switch, {
checked: token.enabled,
onChange: (e, checked) => {
onSetEnabled(token.id, checked);
},
color: 'primary'
})
),
React.createElement(ConfirmDropdown, {
cancelText: formatMessage(translations.confirmCancel),
confirmText: formatMessage(translations.confirmOk),
confirmHelperText: formatMessage(translations.confirmHelperText, {
label: token.label
}),
icon: DeleteIcon,
iconTooltip: React.createElement(FormattedMessage, { defaultMessage: 'Delete' }),
onConfirm: () => {
onDeleteToken(token.id);
}
})
)
)
)
)
)
),
React.createElement(
Typography,
{ variant: 'body2', color: 'textSecondary', sx: { textAlign: 'center', my: 3 } },
React.createElement(FormattedMessage, {
defaultMessage: 'Only API tokens are displayed. Preview tokens are not stored or displayed anywhere.'
})
)
)
: React.createElement(EmptyState, {
title: React.createElement(FormattedMessage, { defaultMessage: 'No Tokens Found' }),
subtitle: React.createElement(FormattedMessage, {
defaultMessage: 'Click Create Token above to create one.'
})
})
),
React.createElement(CreateTokenDialog, {
open: createTokenDialogState.open,
hasPendingChanges: createTokenDialogState.hasPendingChanges,
isSubmitting: createTokenDialogState.isSubmitting,
isMinimized: createTokenDialogState.isMinimized,
onSubmittingAndOrPendingChange: createTokenDialogState.onSubmittingAndOrPendingChange,
onWithPendingChangesCloseRequest: createTokenDialogPendingChangesCloseRequest,
onCreated: onTokenCreated,
onClose: createTokenDialogState.onClose
}),
React.createElement(CreatePreviewTokenDialog, {
open: createPreviewTokenDialogState.open,
hasPendingChanges: createPreviewTokenDialogState.hasPendingChanges,
isSubmitting: createPreviewTokenDialogState.isSubmitting,
isMinimized: createPreviewTokenDialogState.isMinimized,
onSubmittingAndOrPendingChange: createPreviewTokenDialogState.onSubmittingAndOrPendingChange,
onWithPendingChangesCloseRequest: createPreviewTokenDialogPendingChangesCloseRequest,
onClose: createPreviewTokenDialogState.onClose
}),
React.createElement(CopyTokenDialog, {
open: copyTokenDialogState.open,
token: createdToken,
hasPendingChanges: copyTokenDialogState.hasPendingChanges,
isSubmitting: copyTokenDialogState.isSubmitting,
isMinimized: copyTokenDialogState.isMinimized,
onWithPendingChangesCloseRequest: copyTokenDialogPendingChangesCloseRequest,
onClose: onCopyTokenDialogClosed,
onCopy: onTokenCopied
})
);
}
export default TokenManagement;