Created
March 26, 2020 17:32
-
-
Save davidbrenner/2ddeb3c6902f3645f5a11f1780cb19f0 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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