Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2672 forget users #2709

Merged
merged 4 commits into from
May 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions verification/curator-service/api/openapi/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1758,6 +1758,7 @@ components:
type: string
description: Full name of the user if provided by the OAuth provider
example: John Doe
nullable: true
email:
type: string
format: email
Expand Down
3 changes: 2 additions & 1 deletion verification/curator-service/ui/src/components/User.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export default interface User {
_id: string;
name: string;
id?: string; // session user has id, not _id
name?: string | null;
email: string;
roles: string[];
picture?: string;
Expand Down
185 changes: 184 additions & 1 deletion verification/curator-service/ui/src/components/Users.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
import MaterialTable, { QueryResult } from 'material-table';
import { Avatar, Paper, TablePagination, Typography } from '@material-ui/core';
import {
Avatar,
Button,
CircularProgress,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
IconButton,
Menu,
Paper,
TablePagination,
Typography,
} from '@material-ui/core';
import React, { RefObject } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { RootState } from '../redux/store';
Expand All @@ -8,7 +22,10 @@ import {
WithStyles,
createStyles,
withStyles,
makeStyles,
} from '@material-ui/core/styles';
import DeleteIcon from '@material-ui/icons/DeleteOutline';
import MoreVertIcon from '@material-ui/icons/MoreVert';

import FormControl from '@material-ui/core/FormControl';
import MenuItem from '@material-ui/core/MenuItem';
Expand Down Expand Up @@ -69,6 +86,144 @@ const styles = (theme: Theme) =>
},
});

const rowMenuStyles = makeStyles((theme: Theme) => ({
menuItemTitle: {
marginLeft: theme.spacing(1),
},
dialogLoadingSpinner: {
marginRight: theme.spacing(2),
padding: '6px',
},
}));

function RowMenu(props: {
myId: string;
rowId: string;
rowData: TableRow;
setError: (error: string) => void;
refreshData: () => void;
}): JSX.Element {
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const [deleteDialogOpen, setDeleteDialogOpen] =
React.useState<boolean>(false);
const [isDeleting, setIsDeleting] = React.useState(false);
const classes = rowMenuStyles();

const handleClick = (event: React.MouseEvent<HTMLButtonElement>): void => {
event.stopPropagation();
setAnchorEl(event.currentTarget);
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const handleClose = (event?: any): void => {
if (event) {
event.stopPropagation();
}
setAnchorEl(null);
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const openDeleteDialog = (event?: any): void => {
if (event) {
event.stopPropagation();
}
setDeleteDialogOpen(true);
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const handleDelete = async (event?: any): Promise<void> => {
if (event) {
event.stopPropagation();
}
try {
setIsDeleting(true);
props.setError('');
const deleteUrl = '/api/users/' + props.rowId;
await axios.delete(deleteUrl);
if (props.rowId === props.myId) {
/* The user has deleted themselves (alright metaphysicists, their own account).
* We need to redirect them to the homepage, because they can't use the app any more.
* But we also need to end their session, to avoid errors looking up their user.
* When the app can't deserialise the (now-nonexistent) user from the session, it will
* log them out and display the sign up page again.
*/
window.location.replace('/');
}
props.refreshData();
} catch (e) {
props.setError((e as Error).toString());
} finally {
setDeleteDialogOpen(false);
setIsDeleting(false);
handleClose();
}
};

return (
<>
<IconButton
aria-controls="topbar-menu"
aria-haspopup="true"
aria-label="row menu"
data-testid="row menu"
onClick={handleClick}
color="inherit"
>
<MoreVertIcon />
</IconButton>
<Menu
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={handleClose}
>
<MenuItem onClick={openDeleteDialog}>
<DeleteIcon />
<span className={classes.menuItemTitle}>Delete</span>
</MenuItem>
</Menu>
<Dialog
open={deleteDialogOpen}
onClose={(): void => setDeleteDialogOpen(false)}
// Stops the click being propagated to the table which
// would trigger the onRowClick action.
onClick={(e): void => e.stopPropagation()}
>
<DialogTitle>
Are you sure you want to delete this user?
</DialogTitle>
<DialogContent>
<DialogContentText>
User {props.rowData.email} will be permanently deleted.
</DialogContentText>
</DialogContent>
<DialogActions>
{isDeleting ? (
<CircularProgress
classes={{ root: classes.dialogLoadingSpinner }}
/>
) : (
<>
<Button
onClick={(): void => {
setDeleteDialogOpen(false);
}}
color="primary"
autoFocus
>
Cancel
</Button>
<Button onClick={handleDelete} color="primary">
Yes
</Button>
</>
)}
</DialogActions>
</Dialog>
</>
);
}

class Users extends React.Component<Props, UsersState> {
// We could use a proper type here but then we wouldn't be able to call
// onQueryChange() to refresh the table as we want.
Expand Down Expand Up @@ -113,6 +268,34 @@ class Users extends React.Component<Props, UsersState> {
<MaterialTable
tableRef={this.tableRef}
columns={[
...((this.props.user?.roles ?? []).includes('admin')
? [
// TODO: move to the left of selection checkboxes when possible
// https://github.com/mbrn/material-table/issues/2317
{
cellStyle: {
padding: '0',
},
render: (
rowData: TableRow,
): JSX.Element => (
<RowMenu
myId={this.props.user?.id ?? ''}
rowId={rowData.id}
rowData={rowData}
refreshData={(): void =>
this.tableRef.current.onQueryChange()
}
setError={(error): void =>
this.setState({
error: error,
})
}
></RowMenu>
),
},
]
: []),
{
title: 'id',
field: 'id',
Expand Down