rendering users page with the responsive container

This commit is contained in:
Carter 2024-11-25 20:48:48 -06:00
parent 39f5f6d452
commit f2d3b32c97
4 changed files with 139 additions and 184 deletions

View file

@ -67,8 +67,8 @@ export default function UserWrapper(props: Props) {
if (hasFetched.current) return; if (hasFetched.current) return;
setLoading(true) setLoading(true)
userAPI.getUserWithTeam(user_id).then(resp => { userAPI.getUserWithTeam(user_id).then(resp => {
console.log(resp.data.team) // console.log(resp.data.team)
console.log(resp.data.user) // console.log(resp.data.user)
setGlobal({ setGlobal({
user: resp.data.user ? User.create(resp.data.user) : User.create(), user: resp.data.user ? User.create(resp.data.user) : User.create(),
team: resp.data.team ? Team.create(resp.data.team) : Team.create(), team: resp.data.team ? Team.create(resp.data.team) : Team.create(),

View file

@ -1,4 +1,4 @@
import { Box, CircularProgress, Collapse, darken, IconButton, InputAdornment, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TablePagination, TableRow, TextField, Theme, Typography } from "@mui/material"; import { Box, CircularProgress, Collapse, darken, IconButton, InputAdornment, Paper, SxProps, Table, TableBody, TableCell, TableContainer, TableHead, TablePagination, TableRow, TextField, Theme, Typography } from "@mui/material";
import { makeStyles } from "@mui/styles"; import { makeStyles } from "@mui/styles";
import { ChevronRight, Search } from "@mui/icons-material" import { ChevronRight, Search } from "@mui/icons-material"
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@ -28,15 +28,18 @@ const useStyles = makeStyles((theme: Theme) => {
border: "none", border: "none",
textAlign: "right", textAlign: "right",
}, },
addIcon: {
color: "var(--status-ok)"
}
})}, })},
); );
export interface Column<T> {
title: string | JSX.Element;
cellStyle?: SxProps<Theme> | undefined
render?: (row: T) => JSX.Element;
}
interface Props<T> { interface Props<T> {
rows: T[], rows: T[],
columns?: any, columns?: Column<T>[],
title?: string; title?: string;
total: number, total: number,
pageSize: number, pageSize: number,
@ -100,12 +103,12 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
setPage(page) setPage(page)
} }
return( return (
<TableContainer className={classes.tableContainer} component={Paper}> <TableContainer className={classes.tableContainer} component={Paper}>
<Table> <Table>
<TableHead> <TableHead>
<TableRow> <TableRow>
{title && <TableCell colSpan={6} className={classes.titleContainer}> {title && <TableCell colSpan={3} className={classes.titleContainer}>
{typeof(title) === "string" ? {typeof(title) === "string" ?
<Typography variant="h5" className={classes.title}> <Typography variant="h5" className={classes.title}>
{title} {title}
@ -117,7 +120,7 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
} }
</TableCell>} </TableCell>}
{ setSearchText && { setSearchText &&
<TableCell colSpan={6} className={classes.searchContainer}> <TableCell colSpan={title ? 3 : 6} className={classes.searchContainer}>
<TextField <TextField
variant="standard" variant="standard"
className={classes.title} className={classes.title}
@ -137,7 +140,13 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
<TableRow> <TableRow>
{ renderGutter && <TableCell></TableCell>} { renderGutter && <TableCell></TableCell>}
{ columns ? { columns ?
null columns.map((column, index) => {
return (
<TableCell key={"header-cell-"+index}>
{column.title}
</TableCell>
)
})
: rows.length > 0 && : rows.length > 0 &&
Object.keys(rows[0]).map((value, index) => { Object.keys(rows[0]).map((value, index) => {
return( return(
@ -176,7 +185,18 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
</TableCell> </TableCell>
} }
{columns ? {columns ?
null columns.map((column, j) => {
if (column.render) return (
<TableCell key={"row-"+index+"cell-"+j}>
{column.render(row)}
</TableCell>
)
return (
<TableCell key={"row-"+index+"cell-"+j}>
{Object.values(row)[j].toString()}
</TableCell>
)
})
: :
Object.values(row).map((value, j) => { Object.values(row).map((value, j) => {
return ( return (

View file

@ -53,7 +53,7 @@ export default function Router(props: Props) {
{/* Page routes */} {/* Page routes */}
<Route index element={hello()} /> <Route index element={hello()} />
<Route path="/teams" element={<Teams/>} /> <Route path="/teams" element={<Teams/>} />
<Route path="/users" element={<Users title="Users"/>} /> <Route path="/users" element={<Users/>} />
{/* <Route {/* <Route
path="/teams/:teamID" path="/teams/:teamID"
element={<Team />} element={<Team />}

View file

@ -1,13 +1,13 @@
import { import {
Avatar, Avatar,
Box,
Button, Button,
Collapse, Chip,
darken, darken,
DialogActions, DialogActions,
DialogContent, DialogContent,
DialogTitle, DialogTitle,
Divider, Divider,
Grid2,
IconButton, IconButton,
List, List,
ListItem, ListItem,
@ -17,32 +17,19 @@ import {
ListItemText, ListItemText,
ListSubheader, ListSubheader,
PaletteColor, PaletteColor,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TablePagination,
TableRow,
Theme, Theme,
Typography,
} from "@mui/material"; } from "@mui/material";
import ResponsiveDialog from "common/ResponsiveDialog"; import ResponsiveDialog from "common/ResponsiveDialog";
// import { getTableIcons } from "common/ResponsiveTable";
import SearchSelect, { Option } from "common/SearchSelect"; import SearchSelect, { Option } from "common/SearchSelect";
// import SmartBreadcrumb from "common/SmartBreadcrumb"; import { useSnackbar, useUserAPI } from "hooks";
import { useMobile, useSnackbar, useUserAPI } from "hooks";
// import MaterialTable, { Column } from "material-table";
import { User } from "models"; import { User } from "models";
import PageContainer from "pages/PageContainer"; import PageContainer from "pages/PageContainer";
// import { ListUsersResponse } from "providers/pond/userAPI"; // import { ListUsersResponse } from "providers/pond/userAPI";
import React, { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { makeStyles } from "@mui/styles"; import { makeStyles } from "@mui/styles";
import { pond } from "protobuf-ts/pond";
import { getSecondaryColour, getSignatureAccentColour } from "services/whiteLabel"; import { getSecondaryColour, getSignatureAccentColour } from "services/whiteLabel";
import { Add, ChevronRight, Face, Remove } from "@mui/icons-material"; import { Add, Face, Remove } from "@mui/icons-material";
import ResponsiveTable from "common/ResponsiveTable"; import ResponsiveTable, { Column } from "common/ResponsiveTable";
const useStyles = makeStyles((theme: Theme) => { const useStyles = makeStyles((theme: Theme) => {
// const isMobile = useMobile() // const isMobile = useMobile()
@ -98,12 +85,7 @@ const useStyles = makeStyles((theme: Theme) => {
}})}, }})},
); );
interface Props { export default function Users() {
title: string | JSX.Element;
}
export default function Users(props: Props) {
const { title } = props;
const classes = useStyles(); const classes = useStyles();
const userAPI = useUserAPI(); const userAPI = useUserAPI();
// let tableRef: any = React.createRef(); // let tableRef: any = React.createRef();
@ -118,13 +100,6 @@ export default function Users(props: Props) {
const [selectedFeature, setSelectedFeature] = useState<Option | undefined>(); const [selectedFeature, setSelectedFeature] = useState<Option | undefined>();
const [selectedUser, setSelectedUser] = useState(User.create()); const [selectedUser, setSelectedUser] = useState(User.create());
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
const TitleMap = new Map([
["Uploaded", "uploaded"],
["Version", "version"],
["Platform", "platform"],
["Channel", "channel"],
["Size", "size"]
]);
const actions = [ const actions = [
"provision", "provision",
"upload-firmware", "upload-firmware",
@ -153,6 +128,32 @@ export default function Users(props: Props) {
"installer" "installer"
].sort(); ].sort();
const [rows, setRows] = useState<User[]>([])
const loadUsers = () => {
setIsLoading(true)
userAPI.listUsers(pageSize, page*pageSize, "desc", "name", searchText).then(resp => {
let r: User[] = []
resp.users.forEach((user, _index) => {
r.push(user)
})
setRows(r)
setTotal(resp.total)
}).then(() => {
setIsLoading(false);
})
}
useEffect(() => {
loadUsers();
}, [page, pageSize, searchText]); // Trigger fetching data when page or pageSize changes
const handleRowsPerPageChange = (event: any) => {
const newRowsPerPage = parseInt(event.target.value, 10); // Get selected value
setPageSize(newRowsPerPage);
setPage(0); // Reset to the first page
};
const allow = () => { const allow = () => {
const action = selectedAction ? selectedAction.label : ""; const action = selectedAction ? selectedAction.label : "";
if (selectedUser.id() === "" || action === "") return; if (selectedUser.id() === "" || action === "") return;
@ -215,6 +216,64 @@ export default function Users(props: Props) {
}); });
}; };
const flagChip = (flag: any) => {
return (
<Grid2 key={flag} size={{xs: 12}} >
<Chip size="small" label={flag} color={"default"} />
</Grid2>
);
};
const columns = (): Column<User>[] => {
return [
{
title: "Profile",
render: user => {
const avatarURL = user.settings.avatar;
const avatar = avatarURL ? (
<Avatar alt={user.name()} src={avatarURL} />
) : (
<Avatar alt={user.name()}>
<Face />
</Avatar>
);
return (
<ListItem style={{ padding: 0 }}>
<ListItemAvatar>{avatar}</ListItemAvatar>
<ListItemText primary={user.name()} secondary={user.settings.email} />
</ListItem>
);
}
},
{
title: "Actions",
cellStyle: { verticalAlign: "top" },
render: user => {
const actions = user.settings.actions;
return (
<Grid2 container spacing={1} className={classes.chipContainer}>
{actions.slice(0, 2).map(action => flagChip(action))}
{actions.length > 3 && flagChip("+" + (actions.length - 3) + " more")}
</Grid2>
);
}
},
{
title: "Features",
cellStyle: { verticalAlign: "top" },
render: user => {
const features = user.settings.features;
return (
<Grid2 container spacing={1} className={classes.chipContainer}>
{features.slice(0, 2).map(feature => flagChip(feature))}
{features.length > 3 && flagChip("+" + (features.length - 3) + " more")}
</Grid2>
);
}
}
];
};
const details = (user: User) => { const details = (user: User) => {
const item = (text: string, type: "action" | "feature") => ( const item = (text: string, type: "action" | "feature") => (
<ListItem key={text}> <ListItem key={text}>
@ -232,8 +291,6 @@ export default function Users(props: Props) {
); );
const actions = user.settings.actions; const actions = user.settings.actions;
const features = user.settings.features; const features = user.settings.features;
// const showActions = view !== "features";
// const showFeatures = view !== "actions";
return ( return (
<List disablePadding dense> <List disablePadding dense>
<ListSubheader className={classes.gutter}>Actions</ListSubheader> <ListSubheader className={classes.gutter}>Actions</ListSubheader>
@ -269,132 +326,24 @@ export default function Users(props: Props) {
); );
}; };
const renderUser = (user: User) => {
const avatarURL = user.settings.avatar;
const avatar = avatarURL ? (
<Avatar alt={user.name()} src={avatarURL} />
) : (
<Avatar alt={user.name()}>
<Face />
</Avatar>
);
return (
<ListItem style={{ padding: 0 }}>
<ListItemAvatar>{avatar}</ListItemAvatar>
<ListItemText primary={user.name()} secondary={user.settings.email} />
</ListItem>
);
}
// const renderGutter = (user: UserData) => {
// return (
// )
// }
// interface UserData {
// settings: pond.UserSettings;
// }
const [rows, setRows] = useState<User[]>([])
const loadUsers = () => {
setIsLoading(true)
userAPI.listUsers(pageSize, page*pageSize, "desc", "name", searchText).then(resp => {
let r: User[] = []
resp.users.forEach((user, _index) => {
r.push(user)
})
setRows(r)
setTotal(resp.total)
}).then(() => {
setIsLoading(false);
})
}
useEffect(() => {
loadUsers();
}, [page, pageSize, searchText]); // Trigger fetching data when page or pageSize changes
const handleRowsPerPageChange = (event: any) => {
const newRowsPerPage = parseInt(event.target.value, 10); // Get selected value
setPageSize(newRowsPerPage);
setPage(0); // Reset to the first page
};
const handleCollapse = (user: User) => {
if (selectedUser.id() === user.id()) {
setSelectedUser(User.create())
} else {
setSelectedUser(user)
}
}
useEffect(() => {
console.log(isLoading)
}, [isLoading])
return ( return (
<PageContainer> <PageContainer>
<TableContainer className={classes.tableContainer} component={Paper}> <ResponsiveTable<User>
<Table> title="Users"
<TableHead> rows={rows}
<TableRow> columns={columns()}
<TableCell colSpan={6} className={classes.titleContainer}> total={total}
{typeof(title) === "string" ? page={page}
<Typography variant="h5" className={classes.title}> pageSize={pageSize}
{title} setPage={setPage}
</Typography> handleRowsPerPageChange={handleRowsPerPageChange}
: setSearchText={setSearchText}
<Box className={classes.title}> isLoading={isLoading}
title renderGutter={details}
</Box> // multiGutter
} />
</TableCell>
</TableRow>
<TableRow>
<TableCell></TableCell>
<TableCell>Profile</TableCell>
</TableRow>
</TableHead>
<TableBody>
{rows.map((row, index) => (
<React.Fragment key={"user-row-"+index}>
<TableRow>
<TableCell width={1}>
<IconButton sx={{
transform: selectedUser.settings.id === row.settings.id ?
'rotate(90deg)' : 'rotate(0deg)',
transition: 'transform 0.3s ease',
}}
onClick={() => handleCollapse(row)}>
<ChevronRight/>
</IconButton>
</TableCell>
<TableCell>
{renderUser(row)}
</TableCell>
</TableRow>
<TableRow className={classes.gutter}>
<TableCell colSpan={6} style={{padding:0, margin: 0, border: "none"}}>
<Collapse in={selectedUser.settings.id === row.settings.id} timeout="auto" unmountOnExit>
{details(row)}
</Collapse>
</TableCell>
</TableRow>
</React.Fragment>
))}
</TableBody>
</Table>
<TablePagination
rowsPerPageOptions={[5, 10, 20]}
component="div"
count={total}
rowsPerPage={pageSize}
page={page}
onPageChange={(_event, page) => setPage(page)}
onRowsPerPageChange={handleRowsPerPageChange}
/>
</TableContainer>
<ResponsiveDialog <ResponsiveDialog
maxWidth="sm" maxWidth="sm"
fullWidth fullWidth
@ -442,7 +391,6 @@ export default function Users(props: Props) {
<SearchSelect <SearchSelect
label="Select a feature to enable" label="Select a feature to enable"
selected={selectedFeature} selected={selectedFeature}
// options={features}
options={features options={features
.filter(a => !selectedUser.settings.features.includes(a)) .filter(a => !selectedUser.settings.features.includes(a))
.map(a => { .map(a => {
@ -467,19 +415,6 @@ export default function Users(props: Props) {
</Button> </Button>
</DialogActions> </DialogActions>
</ResponsiveDialog> </ResponsiveDialog>
<ResponsiveTable<User>
// title="Users"
rows={rows}
total={total}
page={page}
pageSize={pageSize}
setPage={setPage}
handleRowsPerPageChange={handleRowsPerPageChange}
setSearchText={setSearchText}
isLoading={isLoading}
renderGutter={details}
// multiGutter
/>
</PageContainer> </PageContainer>
); );
} }