Skip to content

Instantly share code, notes, and snippets.

@davidbrenner
Created March 26, 2020 17:32
Show Gist options
  • Save davidbrenner/2ddeb3c6902f3645f5a11f1780cb19f0 to your computer and use it in GitHub Desktop.
Save davidbrenner/2ddeb3c6902f3645f5a11f1780cb19f0 to your computer and use it in GitHub Desktop.
import React, { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
import { makeStyles } from "@material-ui/core/styles";
import Grid from "@material-ui/core/Grid";
import ErrorIcon from "@material-ui/icons/Error";
import PersonOutlineIcon from "@material-ui/icons/PersonOutline";
import { firestore } from "../../firebase";
import EmptyState from "../EmptyState";
import Loader from "../Loader";
import UsersTable from "../UsersTable";
const useStyles = makeStyles({
grid: {
margin: 0,
width: "100%"
}
});
function Users() {
const [loading, setLoading] = useState(true);
const [users, setUsers] = useState(null);
const [size, setSize] = useState(null);
const [error, setError] = useState(null);
const classes = useStyles();
// TODO: optimize perofrmance and utilize cursors
// https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects
// https://firebase.google.com/docs/firestore/query-data/query-cursors
useEffect(() => {
firestore
.collection("users")
.get()
.then(
snapshot => {
let users = [];
snapshot.forEach(function(doc) {
users.push({ ...doc.data(), userDocId: doc.id });
});
setLoading(false);
setUsers(users);
setSize(snapshot.size);
console.log(users);
},
error => {
setLoading(false);
setError(error);
}
);
}, [size]);
if (error) {
return (
<EmptyState
icon={<ErrorIcon />}
title="Something went wrong"
description="There was an error while trying to fetch the requested user"
/>
);
}
if (loading) {
return <Loader />;
}
if (!users) {
return (
<EmptyState
icon={<PersonOutlineIcon />}
title="Users not found"
description="The requested users was not found"
/>
);
}
return (
<Grid className={classes.grid} container justify="center" spacing={5}>
<Grid item xs={12}>
<UsersTable users={users} />
</Grid>
</Grid>
);
}
export default Users;
import React from "react";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableContainer from "@material-ui/core/TableContainer";
import TableHead from "@material-ui/core/TableHead";
import TablePagination from "@material-ui/core/TablePagination";
import TableRow from "@material-ui/core/TableRow";
import TableSortLabel from "@material-ui/core/TableSortLabel";
import Toolbar from "@material-ui/core/Toolbar";
import Typography from "@material-ui/core/Typography";
import Paper from "@material-ui/core/Paper";
import Link from "@material-ui/core/Link";
import Checkbox from "@material-ui/core/Checkbox";
import { lighten, makeStyles } from "@material-ui/core/styles";
import clsx from "clsx";
import IconButton from "@material-ui/core/IconButton";
import Tooltip from "@material-ui/core/Tooltip";
import DeleteIcon from "@material-ui/icons/Delete";
import FilterListIcon from "@material-ui/icons/FilterList";
import { functions, firestore } from "../../firebase";
import PropTypes from "prop-types";
function createData(
userDocId,
email,
firstName,
optedIn,
paired,
signupCompleted,
frequency,
partnerURL,
partnerEmail,
planName
) {
return {
userDocId,
email,
firstName,
optedIn,
paired,
signupCompleted,
frequency,
partnerURL,
partnerEmail,
planName
};
}
function descendingComparator(a, b, orderBy) {
if (b[orderBy] < a[orderBy]) {
return -1;
}
if (b[orderBy] > a[orderBy]) {
return 1;
}
return 0;
}
function getComparator(order, orderBy) {
return order === "desc"
? (a, b) => descendingComparator(a, b, orderBy)
: (a, b) => -descendingComparator(a, b, orderBy);
}
function stableSort(array, comparator) {
const stabilizedThis = array.map((el, index) => [el, index]);
stabilizedThis.sort((a, b) => {
const order = comparator(a[0], b[0]);
if (order !== 0) return order;
return a[1] - b[1];
});
return stabilizedThis.map(el => el[0]);
}
const headCells = [
{
id: "userDocId",
numeric: false,
disablePadding: true,
label: "User ID"
},
{
id: "email",
numeric: true,
disablePadding: false,
label: "Email"
},
{
id: "firstName",
numeric: true,
disablePadding: true,
label: "First Name"
},
{
id: "optedIn",
numeric: true,
disablePadding: true,
label: "Opted In?"
},
{
id: "paired",
numeric: true,
disablePadding: true,
label: "Paired?"
},
{
id: "signupCompleted",
numeric: true,
disablePadding: true,
label: "Sign Up Completed?"
},
{
id: "frequency",
numeric: true,
disablePadding: true,
label: "Question Frequency"
},
{
id: "partnerURL",
numeric: true,
disablePadding: true,
label: "Partner URL"
},
{
id: "partnerEmail",
numeric: true,
disablePadding: true,
label: "Partner Email"
},
{
id: "planName",
numeric: true,
disablePadding: true,
label: "Plan Name"
}
];
function UsersTableHead(props) {
const {
classes,
onSelectAllClick,
order,
orderBy,
numSelected,
rowCount,
onRequestSort
} = props;
const createSortHandler = property => event => {
onRequestSort(event, property);
};
return (
<TableHead>
<TableRow>
<TableCell padding="checkbox">
<Checkbox
indeterminate={numSelected > 0 && numSelected < rowCount}
checked={rowCount > 0 && numSelected === rowCount}
onChange={onSelectAllClick}
inputProps={{ "aria-label": "select all users" }}
/>
</TableCell>
{headCells.map(headCell => (
<TableCell
key={headCell.id}
align={headCell.numeric ? "right" : "left"}
padding={headCell.disablePadding ? "none" : "default"}
sortDirection={orderBy === headCell.id ? order : false}
>
<TableSortLabel
active={orderBy === headCell.id}
direction={orderBy === headCell.id ? order : "asc"}
onClick={createSortHandler(headCell.id)}
>
{headCell.label}
{orderBy === headCell.id ? (
<span className={classes.visuallyHidden}>
{order === "desc" ? "sorted descending" : "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
))}
</TableRow>
</TableHead>
);
}
UsersTableHead.propTypes = {
classes: PropTypes.object.isRequired,
numSelected: PropTypes.number.isRequired,
onRequestSort: PropTypes.func.isRequired,
onSelectAllClick: PropTypes.func.isRequired,
order: PropTypes.oneOf(["asc", "desc"]).isRequired,
orderBy: PropTypes.string.isRequired,
rowCount: PropTypes.number.isRequired
};
const useToolbarStyles = makeStyles(theme => ({
root: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(1)
},
highlight:
theme.palette.type === "light"
? {
color: theme.palette.secondary.main,
backgroundColor: lighten(theme.palette.secondary.light, 0.85)
}
: {
color: theme.palette.text.primary,
backgroundColor: theme.palette.secondary.dark
},
title: {
flex: "1 1 100%"
}
}));
function deleteAtPath(path) {
var deleteFn = functions.httpsCallable("recursiveDelete");
deleteFn({ path: path })
.then(function(result) {
console.log("Delete success: " + JSON.stringify(result));
})
.catch(function(err) {
console.log("Delete failed, see console,", path);
console.warn(err);
});
}
function deleteUser(userToDelete) {
var deleteFn = functions.httpsCallable("deleteUser");
deleteFn({ userToDelete: userToDelete })
.then(function(result) {
console.log("Delete success: " + JSON.stringify(result));
})
.catch(function(err) {
console.log("Delete failed, see console,", userToDelete);
console.warn(err);
});
}
const UsersTableToolbar = props => {
const classes = useToolbarStyles();
const { numSelected, selected } = props;
const handleDelete = async () => {
console.log("selected:", selected);
for (let uid of selected) {
console.log("preparing to delete:", uid);
const userDoc = await firestore.doc(`/users/${uid}`).get();
const coupleID = userDoc.data().coupleID;
const mobile = userDoc.data().mobile;
const partnerURL = userDoc.data().partnerURL;
const referralURL = userDoc.data().referralURL;
console.log("got user:", userDoc.data());
if (typeof coupleID !== "undefined") {
console.log(`Delete /couples/${coupleID}`);
await deleteAtPath(`/couples/${coupleID}`);
console.log(`Deleted /couples/${coupleID}`);
}
if (typeof mobile !== "undefined") {
console.log(`Delete /sms_users/${mobile}`);
await deleteAtPath(`/sms_users/${mobile}`);
console.log(`Deleted /sms_users/${mobile}`);
}
if (typeof partnerURL !== "undefined") {
console.log(`Delete /partnerURL/${partnerURL}`);
await deleteAtPath(`/partnerURL/${partnerURL}`);
console.log(`Deleted /partnerURL/${partnerURL}`);
}
await deleteAtPath(`/users/${uid}`);
await deleteAtPath(`/stripe_users/${uid}`);
await deleteUser(uid);
console.log(`Deleted user: ${uid}`);
}
};
return (
<Toolbar
className={clsx(classes.root, {
[classes.highlight]: numSelected > 0
})}
>
{numSelected > 0 ? (
<Typography
className={classes.title}
color="inherit"
variant="subtitle1"
>
{numSelected} selected
</Typography>
) : (
<Typography className={classes.title} variant="h6" id="tableTitle">
Users
</Typography>
)}
{numSelected > 0 ? (
<Tooltip title="Delete">
<IconButton aria-label="delete" onClick={handleDelete}>
<DeleteIcon />
</IconButton>
</Tooltip>
) : (
<Tooltip title="Filter list">
<IconButton aria-label="filter list">
<FilterListIcon />
</IconButton>
</Tooltip>
)}
</Toolbar>
);
};
UsersTableToolbar.propTypes = {
numSelected: PropTypes.number.isRequired
};
const useStyles = makeStyles(theme => ({
root: {
width: "100%"
},
paper: {
width: "100%",
marginBottom: theme.spacing(2)
},
table: {
minWidth: 750
},
visuallyHidden: {
border: 0,
clip: "rect(0 0 0 0)",
height: 1,
margin: -1,
overflow: "hidden",
padding: 0,
position: "absolute",
top: 20,
width: 1
}
}));
export default function UsersTable(props) {
const classes = useStyles();
const users = props.users;
const [order, setOrder] = React.useState("asc");
const [orderBy, setOrderBy] = React.useState("userDocId");
const [selected, setSelected] = React.useState([]);
const [page, setPage] = React.useState(0);
const [rowsPerPage, setRowsPerPage] = React.useState(10);
const handleRequestSort = (event, property) => {
const isAsc = orderBy === property && order === "asc";
setOrder(isAsc ? "desc" : "asc");
setOrderBy(property);
};
const handleSelectAllClick = event => {
if (event.target.checked) {
const newSelecteds = users.map(n => n.userDocId);
setSelected(newSelecteds);
return;
}
setSelected([]);
};
const handleClick = (event, userDocId) => {
const selectedIndex = selected.indexOf(userDocId);
let newSelected = [];
if (selectedIndex === -1) {
newSelected = newSelected.concat(selected, userDocId);
} else if (selectedIndex === 0) {
newSelected = newSelected.concat(selected.slice(1));
} else if (selectedIndex === selected.length - 1) {
newSelected = newSelected.concat(selected.slice(0, -1));
} else if (selectedIndex > 0) {
newSelected = newSelected.concat(
selected.slice(0, selectedIndex),
selected.slice(selectedIndex + 1)
);
}
setSelected(newSelected);
};
const handleChangePage = (event, newPage) => {
setPage(newPage);
};
const handleChangeRowsPerPage = event => {
setRowsPerPage(parseInt(event.target.value, 10));
setPage(0);
};
const isSelected = userDocId => selected.indexOf(userDocId) !== -1;
const emptyRows =
rowsPerPage - Math.min(rowsPerPage, users.length - page * rowsPerPage);
return (
<div className={classes.root}>
<Paper className={classes.paper}>
<UsersTableToolbar numSelected={selected.length} selected={selected} />
<TableContainer>
<Table
className={classes.table}
aria-labelledby="tableTitle"
size="medium"
aria-label="enhanced table"
>
<UsersTableHead
classes={classes}
numSelected={selected.length}
order={order}
orderBy={orderBy}
onSelectAllClick={handleSelectAllClick}
onRequestSort={handleRequestSort}
rowCount={users.length}
/>
<TableBody>
{stableSort(users, getComparator(order, orderBy))
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
.map((row, index) => {
const isItemSelected = isSelected(row.userDocId);
const labelId = `enhanced-table-checkbox-${index}`;
return (
<TableRow
hover
onClick={event => handleClick(event, row.userDocId)}
role="checkbox"
aria-checked={isItemSelected}
tabIndex={-1}
key={row.userDocId}
selected={isItemSelected}
>
<TableCell padding="checkbox">
<Checkbox
checked={isItemSelected}
inputProps={{ "aria-labelledby": labelId }}
/>
</TableCell>
<TableCell
component="th"
id={labelId}
scope="row"
padding="none"
>
<Link href={"users/" + row.userDocId}>
{row.userDocId}
</Link>
</TableCell>
<TableCell align="right">
{typeof row.email !== "undefined" ? row.email : "-"}
</TableCell>
<TableCell align="right">
{typeof row.firstName !== "undefined"
? row.firstName
: "-"}
</TableCell>
<TableCell align="right">
{typeof row.optedIn !== "undefined"
? row.optedIn.toString()
: "-"}
</TableCell>
<TableCell align="right">
{typeof row.paired !== "undefined"
? row.paired.toString()
: "-"}
</TableCell>
<TableCell align="right">
{typeof row.signUpCompleted !== "undefined"
? row.signUpCompleted.toString()
: "-"}
</TableCell>
<TableCell align="right">
{typeof row.frequency !== "undefined"
? row.frequency
: "-"}
</TableCell>
<TableCell align="right">
{typeof row.partnerURL !== "undefined"
? row.partnerURL
: "-"}
</TableCell>
<TableCell align="right">
{typeof row.partnerEmail !== "undefined"
? row.partnerEmail
: "-"}
</TableCell>
<TableCell align="right">
{typeof row.planName !== "undefined"
? row.planName
: "-"}
</TableCell>
</TableRow>
);
})}
{emptyRows > 0 && (
<TableRow style={{ height: 53 * emptyRows }}>
<TableCell colSpan={6} />
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[5, 10, 25]}
component="div"
count={users.length}
rowsPerPage={rowsPerPage}
page={page}
onChangePage={handleChangePage}
onChangeRowsPerPage={handleChangeRowsPerPage}
/>
</Paper>
</div>
);
}
UsersTable.propTypes = {
users: PropTypes.array.isRequired
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment