building data grid for users. currently has example grid with dummy data

This commit is contained in:
Carter 2024-11-21 13:03:02 -06:00
parent 2690a2a5ad
commit d050fdee55
8 changed files with 780 additions and 632 deletions

1099
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -23,6 +23,7 @@
"@mui/icons-material": "^6.1.6", "@mui/icons-material": "^6.1.6",
"@mui/material": "^6.1.6", "@mui/material": "^6.1.6",
"@mui/styles": "^6.1.6", "@mui/styles": "^6.1.6",
"@mui/x-data-grid": "^7.22.2",
"@mui/x-date-pickers": "^7.22.2", "@mui/x-date-pickers": "^7.22.2",
"@sentry/react": "^8.38.0", "@sentry/react": "^8.38.0",
"@types/classnames": "^2.3.0", "@types/classnames": "^2.3.0",
@ -35,6 +36,7 @@
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-image": "^4.1.0", "react-image": "^4.1.0",
"react-infinite-scroller": "^1.2.6",
"react-router-dom": "^6.28.0" "react-router-dom": "^6.28.0"
}, },
"devDependencies": { "devDependencies": {
@ -43,6 +45,7 @@
"@types/lodash": "^4.17.13", "@types/lodash": "^4.17.13",
"@types/react": "^18.3.10", "@types/react": "^18.3.10",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"@types/react-infinite-scroller": "^1.2.5",
"@vitejs/plugin-react": "^4.3.2", "@vitejs/plugin-react": "^4.3.2",
"eslint": "^9.11.1", "eslint": "^9.11.1",
"eslint-plugin-react-hooks": "^5.1.0-rc.0", "eslint-plugin-react-hooks": "^5.1.0-rc.0",

View file

@ -67,6 +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.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(),

88
src/common/Loader.tsx Normal file
View file

@ -0,0 +1,88 @@
import { Theme, useTheme } from "@mui/material";
import { makeStyles } from "@mui/styles";
import classnames from "classnames";
import { getDarkLogo, getLightLogo } from "services/whiteLabel";
const useStyles = makeStyles((theme: Theme) => {
const smallSize = theme.breakpoints.down("sm") ? 32 : theme.breakpoints.up("lg") ? 64 : 48;
return ({
loader: {
height: "20vh",
width: "20vh",
position: "relative"
},
"@keyframes spin": {
from: {
transform: "rotate(0deg)"
},
to: {
transform: "rotate(360deg)"
}
},
spinner: {
position: "absolute",
width: "20vh",
height: "20vh",
lineHeight: "20vh",
margin: "0 auto",
borderRadius: "50%",
zIndex: 9,
animation: "$spin 0.75s linear infinite"
},
lightSpinner: {
borderTop: "0.25rem solid #fff"
},
darkSpinner: {
borderTop: "0.25rem solid #404040"
},
spinnerLogo: {
display: "block",
height: "8vh",
position: "absolute",
left: "50%",
top: "50%",
transform: "translate(-50%, -50%)"
},
smallLoader: {
height: smallSize,
width: smallSize
},
smallSpinner: {
width: smallSize,
height: smallSize,
lineHeight: smallSize,
borderTopWidth: theme.spacing(0.25)
},
smallSpinnerLogo: {
height: smallSize / 2
}
});
});
interface Props {
size?: "small" | "default";
}
export default function Loader(props: Props) {
const classes = useStyles();
const theme = useTheme();
const { size } = props;
const isDarkTheme = theme.palette.mode === "dark";
return (
<div className={classnames(classes.loader, size === "small" && classes.smallLoader)}>
<div
className={classnames(
classes.spinner,
isDarkTheme ? classes.lightSpinner : classes.darkSpinner,
size === "small" && classes.smallSpinner
)}
/>
<img
className={classnames(classes.spinnerLogo, size === "small" && classes.smallSpinnerLogo)}
src={isDarkTheme ? getLightLogo() : getDarkLogo()}
alt={process.env.REACT_APP_WEBSITE_TITLE + "-loader"}
/>
</div>
);
}

182
src/common/SearchSelect.tsx Normal file
View file

@ -0,0 +1,182 @@
import { Autocomplete, Avatar, CircularProgress, createFilterOptions } from "@mui/material";
import TextField from "@mui/material/TextField";
import { makeStyles } from "@mui/styles";
// import Autocomplete, { createFilterOptions } from "@material-ui/lab/Autocomplete";
import { isArray } from "lodash";
import React, { CSSProperties } from "react";
const useStyles = makeStyles(() => ({
input: {
minWidth: `0 !important`
}})
);
export interface Option {
value: any;
label: string;
group?: string;
new?: boolean;
icon?: string;
defaultCableID?: number;
}
interface Props {
selected?: Option | Option[] | null;
changeSelection?: (newSelected: Option | null) => void;
changeMulti?: (newSelected: Option[] | null) => void;
options: Option[];
label?: string;
group?: boolean;
creatable?: boolean;
loading?: boolean;
disabled?: boolean;
multiple?: boolean;
variant?: "filled" | "outlined" | "standard";
style?: CSSProperties;
getOptionSelected?: any;
onChange?: (e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => void;
onKeyDown?: (e: React.KeyboardEvent<HTMLDivElement>) => void;
}
export default function SearchSelect(props: Props) {
const {
options,
changeSelection,
changeMulti,
selected,
label,
creatable,
loading,
group,
disabled,
multiple,
variant,
style,
getOptionSelected,
onChange,
onKeyDown
} = props;
const classes = useStyles();
const filter = createFilterOptions<Option>({
stringify: (option: Option) => option.label + option.group
});
const stringToOption = (s: string): Option => {
return { value: s, label: s } as Option;
};
return (
<Autocomplete
id={label + "-autocomplete"}
value={selected === undefined ? null : selected}
multiple={multiple}
limitTags={2}
autoHighlight
disabled={disabled}
onChange={(_, newValue) => {
if (multiple === true) {
if (changeMulti) {
if (Array.isArray(newValue)) {
changeMulti(newValue.map(v => (typeof v === "string" ? stringToOption(v) : v)));
} else {
changeMulti(
newValue
? [typeof newValue === "string" ? stringToOption(newValue) : newValue]
: null
);
}
} else {
console.warn("SearchSelect requires changeMulti to be defined when 'multiple' is true");
}
} else {
if (changeSelection) {
if (Array.isArray(newValue)) {
console.warn("changeSelection requires a single element but received an array");
} else {
changeSelection(typeof newValue === "string" ? stringToOption(newValue) : newValue);
}
} else {
console.warn(
"SearchSelect requires changeSelection to be defined when 'multiple' is false"
);
}
}
}}
options={options.sort((a, b) => {
let aGroup = a.group ? a.group : "";
let bGroup = b.group ? b.group : "";
if (bGroup > aGroup) return -1;
if (aGroup > bGroup) return 1;
return b.label > a.label ? -1 : 1;
})}
filterOptions={(options, params) => {
const filtered = filter(options as Option[], params);
if (creatable && params.inputValue !== "") {
filtered.push({
value: params.inputValue,
label: params.inputValue,
new: true
});
}
return filtered;
}}
getOptionLabel={option => {
if (option instanceof Option) {
return option.text + " - " + option.value
} else {
return option as string
}
}}
// getOptionSelected={
// getOptionSelected ? getOptionSelected : (o: { value: any; }, v: { value: any; }) => (o && v ? o.value === v.value : false)
// }
groupBy={group ? option => (option && option.group ? option.group : "") : undefined}
renderInput={params => (
<TextField
{...params}
label={label}
variant={variant ? variant : "outlined"}
onChange={onChange}
onKeyDown={onKeyDown}
InputProps={{
...params.InputProps,
startAdornment:
!isArray(selected) && selected?.icon ? <Avatar src={selected?.icon} /> : null,
endAdornment: (
<React.Fragment>
{loading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment}
</React.Fragment>
)
}}
/>
)}
// renderOption={params => (
// <div
// style={{
// display: "flex",
// flexDirection: "row"
// }}>
// <div style={{ marginTop: "auto", marginBottom: "auto" }}>
// {!isArray(params) && params?.icon ? <Avatar src={params?.icon} /> : ""}
// </div>
// <div style={{ marginTop: "auto", marginBottom: "auto", marginLeft: "1rem" }}>
// {params.label}
// </div>
// </div>
// )}
loading={loading}
selectOnFocus
clearOnBlur
freeSolo={creatable}
handleHomeEndKeys
fullWidth
classes={{
input: classes.input
}}
style={style}
/>
);
}

View file

@ -5,6 +5,7 @@ import { Typography } from "@mui/material";
import { useAuth0 } from "@auth0/auth0-react"; import { useAuth0 } from "@auth0/auth0-react";
import Teams from "pages/Teams"; import Teams from "pages/Teams";
import SideNavigator from "./SideNavigator"; import SideNavigator from "./SideNavigator";
import Users from "pages/Users";
interface Props { interface Props {
open: boolean, open: boolean,
@ -52,6 +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/>} />
{/* <Route {/* <Route
path="/teams/:teamID" path="/teams/:teamID"
element={<Team />} element={<Team />}

View file

@ -1,4 +1,4 @@
import { ChevronRight, People, PeopleOutline, PeopleOutlined, PeopleSharp } from "@mui/icons-material"; import { ChevronRight, People, PeopleOutline, PeopleOutlined, PeopleSharp, Person } from "@mui/icons-material";
import ChevronLeft from "@mui/icons-material/ChevronLeft"; import ChevronLeft from "@mui/icons-material/ChevronLeft";
import { darken, Divider, Grid2 as Grid, IconButton, lighten, List, ListItemButton, ListItemIcon, ListItemText, SwipeableDrawer, Theme, Toolbar, Tooltip, useTheme } from "@mui/material"; import { darken, Divider, Grid2 as Grid, IconButton, lighten, List, ListItemButton, ListItemIcon, ListItemText, SwipeableDrawer, Theme, Toolbar, Tooltip, useTheme } from "@mui/material";
import classNames from "classnames"; import classNames from "classnames";
@ -132,6 +132,20 @@ export default function SideNavigator(props: Props) {
{open && <ListItemText primary="Teams" />} {open && <ListItemText primary="Teams" />}
</ListItemButton> </ListItemButton>
</Tooltip> </Tooltip>
<Tooltip title="Users" placement="right">
<ListItemButton
id="tour-dashboard"
component={Link}
to="/users"
onClick={onClose}
classes={getClasses("/user")}
>
<ListItemIcon>
<Person />
</ListItemIcon>
{open && <ListItemText primary="Users" />}
</ListItemButton>
</Tooltip>
</List> </List>
) )
} }

View file

@ -40,7 +40,7 @@ export interface IUserAPIContext {
// getUserSettings: (userID: string) => Promise<any>; // getUserSettings: (userID: string) => Promise<any>;
// updateUserSettings: (userID: string, body: pond.IUserSettings) => Promise<any>; // updateUserSettings: (userID: string, body: pond.IUserSettings) => Promise<any>;
// updateUserStatus: (userID: string, body: pond.IUserStatus) => Promise<any>; // updateUserStatus: (userID: string, body: pond.IUserStatus) => Promise<any>;
// updateUser: (id: string, body: pond.User) => Promise<any>; updateUser: (id: string, body: pond.User) => Promise<any>;
// getUserObjectHistory: ( // getUserObjectHistory: (
// id: string, // id: string,
// objects: number[], // objects: number[],
@ -208,9 +208,9 @@ export default function UserProvider(props: PropsWithChildren<any>) {
// return put(pondURL("/users/" + userID + "/status"), body); // return put(pondURL("/users/" + userID + "/status"), body);
// }; // };
// const updateUser = (id: string, body: pond.User) => { const updateUser = (id: string, body: pond.User) => {
// return put(pondURL("/users/" + id), body); return put(pondURL("/users/" + id), body);
// }; };
// const getUserObjectHistory = (id: string, objects: number[], start: string, end: string) => { // const getUserObjectHistory = (id: string, objects: number[], start: string, end: string) => {
// return get<pond.GetUserObjectHistoriesResponse>( // return get<pond.GetUserObjectHistoriesResponse>(
@ -242,7 +242,7 @@ export default function UserProvider(props: PropsWithChildren<any>) {
// getUserSettings, // getUserSettings,
// updateUserSettings, // updateUserSettings,
// updateUserStatus, // updateUserStatus,
// updateUser, updateUser,
// getUserObjectHistory // getUserObjectHistory
}}> }}>
{children} {children}