440 lines
13 KiB
TypeScript
440 lines
13 KiB
TypeScript
import {
|
|
Avatar,
|
|
Box,
|
|
Button,
|
|
Chip,
|
|
darken,
|
|
DialogActions,
|
|
DialogContent,
|
|
DialogTitle,
|
|
Divider,
|
|
Grid2,
|
|
IconButton,
|
|
List,
|
|
ListItem,
|
|
ListItemAvatar,
|
|
ListItemButton,
|
|
ListItemIcon,
|
|
ListItemText,
|
|
ListSubheader,
|
|
PaletteColor,
|
|
Theme,
|
|
} from "@mui/material";
|
|
import ResponsiveDialog from "common/ResponsiveDialog";
|
|
import SearchSelect, { Option } from "common/SearchSelect";
|
|
import { useMobile, useSnackbar, useUserAPI } from "hooks";
|
|
import { User } from "models";
|
|
import PageContainer from "pages/PageContainer";
|
|
// import { ListUsersResponse } from "providers/pond/userAPI";
|
|
import { useEffect, useState } from "react";
|
|
import { makeStyles } from "@mui/styles";
|
|
import { getSecondaryColour, getSignatureAccentColour } from "services/whiteLabel";
|
|
import { Add, Face, Remove } from "@mui/icons-material";
|
|
import ResponsiveTable, { Column } from "common/ResponsiveTable";
|
|
|
|
const useStyles = makeStyles((theme: Theme) => {
|
|
// const isMobile = useMobile()
|
|
return ({
|
|
gutter: {
|
|
backgroundColor: darken(theme.palette.background.paper, 0.03),
|
|
border: "none"
|
|
},
|
|
dataTable: {
|
|
backgroundColor: theme.palette.background.paper
|
|
},
|
|
tableContainer: {
|
|
margin: theme.spacing(1),
|
|
[theme.breakpoints.up("sm")]: {
|
|
margin: theme.spacing(2)
|
|
},
|
|
width: "auto"
|
|
},
|
|
title: {
|
|
padding: theme.spacing(1)
|
|
},
|
|
titleContainer: {
|
|
border: "none"
|
|
},
|
|
chipContainer: {
|
|
maxWidth: "100%",
|
|
maxHeight: "100%",
|
|
justifyContent: "flex-start",
|
|
alignItems: "flex-start"
|
|
},
|
|
deleteIcon: {
|
|
color: "var(--status-alert)"
|
|
},
|
|
userAvatar: {
|
|
color: "#fff",
|
|
backgroundColor: getSecondaryColour()["700" as keyof PaletteColor],
|
|
[theme.breakpoints.down("xs")]: {
|
|
width: "32px",
|
|
height: "32px"
|
|
}
|
|
},
|
|
listItemButton: {
|
|
'&:hover': {
|
|
color: getSignatureAccentColour()
|
|
}
|
|
},
|
|
avatarContainer: {
|
|
textAlign: "center",
|
|
justifyContent: "center",
|
|
},
|
|
addIcon: {
|
|
color: "var(--status-ok)"
|
|
}})},
|
|
);
|
|
|
|
export default function Users() {
|
|
const classes = useStyles();
|
|
const isMobile = useMobile();
|
|
const userAPI = useUserAPI();
|
|
// let tableRef: any = React.createRef();
|
|
const { error, success } = useSnackbar();
|
|
const [pageSize, setPageSize] = useState(5);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [page, setPage] = useState(0);
|
|
const [total, setTotal] = useState(0);
|
|
const [showAllowAction, setShowAllowAction] = useState(false);
|
|
const [selectedAction, setSelectedAction] = useState<Option | undefined>();
|
|
const [showEnableFeature, setShowEnableFeature] = useState(false);
|
|
const [selectedFeature, setSelectedFeature] = useState<Option | undefined>();
|
|
const [selectedUser, setSelectedUser] = useState(User.create());
|
|
const [searchText, setSearchText] = useState("");
|
|
const actions = [
|
|
"provision",
|
|
"upload-firmware",
|
|
"remove-devices",
|
|
"copy-token",
|
|
"recluse",
|
|
"pause-data"
|
|
].sort();
|
|
const features = [
|
|
"admin",
|
|
"beta",
|
|
"billing",
|
|
"security",
|
|
"dev-channel",
|
|
"docs",
|
|
"sleep",
|
|
"support",
|
|
"tasks",
|
|
"teams",
|
|
"maps",
|
|
"json",
|
|
"john-deere",
|
|
"grain-composition",
|
|
"developer",
|
|
"marketplace",
|
|
"installer",
|
|
"cnhi"
|
|
].sort();
|
|
|
|
const [rows, setRows] = useState<User[]>([])
|
|
|
|
const loadUsers = () => {
|
|
setIsLoading(true)
|
|
userAPI.listUsers(pageSize, page*pageSize, "desc", "name", undefined, 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 action = selectedAction ? selectedAction.label : "";
|
|
if (selectedUser.id() === "" || action === "") return;
|
|
if (!selectedUser.settings.actions.includes(action)) {
|
|
selectedUser.settings.actions.push(action);
|
|
}
|
|
userAPI
|
|
.updateUser(selectedUser.id(), selectedUser.protobuf())
|
|
.then(() => {
|
|
success("Allowed " + selectedUser.name() + " to " + action);
|
|
setShowAllowAction(false);
|
|
setSelectedAction({} as Option);
|
|
})
|
|
.catch(() => error("Failed to allow " + selectedUser.name() + " to " + action));
|
|
};
|
|
|
|
const enable = () => {
|
|
const feature = selectedFeature ? selectedFeature.label : "";
|
|
if (selectedUser.id() === "" || feature === "") return;
|
|
if (!selectedUser.settings.features.includes(feature)) {
|
|
selectedUser.settings.features.push(feature);
|
|
}
|
|
userAPI
|
|
.updateUser(selectedUser.id(), selectedUser.protobuf())
|
|
.then(() => {
|
|
success("Enabled " + feature + " for " + selectedUser.name());
|
|
setShowEnableFeature(false);
|
|
setSelectedFeature({} as Option);
|
|
})
|
|
.catch(() => error("Failed to enable " + feature + " for " + selectedUser.name()));
|
|
};
|
|
|
|
const remove = (user: User, type: "action" | "feature", key: string) => {
|
|
if (user.id() === "" || key === "") return;
|
|
if (type === "action") {
|
|
user.settings.actions = user.settings.actions.filter(v => v !== key);
|
|
}
|
|
if (type === "feature") {
|
|
user.settings.features = user.settings.features.filter(v => v !== key);
|
|
}
|
|
userAPI
|
|
.updateUser(user.id(), user.protobuf())
|
|
.then(() => {
|
|
if (type === "action") {
|
|
success("Disallowed " + user.name() + " from " + key);
|
|
setSelectedAction({} as Option);
|
|
}
|
|
if (type === "feature") {
|
|
success("Disabled " + key + " for " + user.name());
|
|
setSelectedFeature({} as Option);
|
|
}
|
|
})
|
|
.catch(() => {
|
|
if (type === "action") {
|
|
error("Failed to disallow " + user.name() + " from " + key);
|
|
}
|
|
if (type === "feature") {
|
|
error("Failed to disable " + key + " for " + user.name());
|
|
}
|
|
});
|
|
};
|
|
|
|
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 sx={{ 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} padding={1}>
|
|
{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} padding={1}>
|
|
{features.slice(0, 2).map(feature => flagChip(feature))}
|
|
{features.length > 3 && flagChip("+" + (features.length - 3) + " more")}
|
|
</Grid2>
|
|
);
|
|
}
|
|
}
|
|
];
|
|
};
|
|
|
|
const details = (user: User) => {
|
|
const item = (text: string, type: "action" | "feature") => (
|
|
<ListItem key={text}>
|
|
<ListItemIcon>
|
|
<IconButton
|
|
onClick={() => {
|
|
setSelectedUser(user);
|
|
remove(user, type, text);
|
|
}}>
|
|
<Remove className={classes.deleteIcon} />
|
|
</IconButton>
|
|
</ListItemIcon>
|
|
<ListItemText>{text}</ListItemText>
|
|
</ListItem>
|
|
);
|
|
const actions = user.settings.actions;
|
|
const features = user.settings.features;
|
|
return (
|
|
<List disablePadding dense>
|
|
<ListSubheader className={classes.gutter}>Actions</ListSubheader>
|
|
{actions.map(action => item(action, "action"))}
|
|
<ListItemButton
|
|
onClick={() => {
|
|
setSelectedUser(user);
|
|
setShowAllowAction(true);
|
|
}}>
|
|
<ListItemIcon>
|
|
<IconButton>
|
|
<Add className={classes.addIcon} />
|
|
</IconButton>
|
|
</ListItemIcon>
|
|
<ListItemText>Allow an action</ListItemText>
|
|
</ListItemButton>
|
|
<Divider />
|
|
<ListSubheader className={classes.gutter}>Features</ListSubheader>
|
|
{features.map(feature => item(feature, "feature"))}
|
|
<ListItemButton
|
|
onClick={() => {
|
|
setSelectedUser(user);
|
|
setShowEnableFeature(true);
|
|
}}>
|
|
<ListItemIcon>
|
|
<IconButton>
|
|
<Add className={classes.addIcon} />
|
|
</IconButton>
|
|
</ListItemIcon>
|
|
<ListItemText>Enable a feature</ListItemText>
|
|
</ListItemButton>
|
|
</List>
|
|
);
|
|
};
|
|
|
|
const renderMobile = (user: User) => {
|
|
const avatarURL = user.settings.avatar;
|
|
const avatar = avatarURL ? (
|
|
<Avatar alt={user.name()} src={avatarURL} />
|
|
) : (
|
|
<Avatar alt={user.name()}>
|
|
<Face />
|
|
</Avatar>
|
|
);
|
|
return (
|
|
<Box sx={{padding: 1}}>
|
|
<ListItem>
|
|
<ListItemAvatar>{avatar}</ListItemAvatar>
|
|
<ListItemText primary={user.name()} secondary={user.settings.email} />
|
|
</ListItem>
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<PageContainer padding={isMobile ? 0 : 2}>
|
|
<ResponsiveTable<User>
|
|
title="Users"
|
|
rows={rows}
|
|
columns={columns}
|
|
total={total}
|
|
page={page}
|
|
pageSize={pageSize}
|
|
setPage={setPage}
|
|
handleRowsPerPageChange={handleRowsPerPageChange}
|
|
setSearchText={setSearchText}
|
|
isLoading={isLoading}
|
|
renderGutter={details}
|
|
renderMobile={renderMobile}
|
|
// multiGutter
|
|
/>
|
|
<ResponsiveDialog
|
|
maxWidth="sm"
|
|
fullWidth
|
|
open={showAllowAction}
|
|
onClose={() => setShowAllowAction(false)}
|
|
aria-labelledby="allow-action">
|
|
<DialogTitle id="allow-action-title">Allow an action</DialogTitle>
|
|
<DialogContent>
|
|
<SearchSelect
|
|
label="Select an action to allow"
|
|
selected={selectedAction}
|
|
options={actions
|
|
.filter(a => !selectedUser.settings.actions.includes(a))
|
|
.map(a => {
|
|
return { value: a, label: a };
|
|
})}
|
|
changeSelection={(option: Option | null) =>
|
|
setSelectedAction(option ? option : undefined)
|
|
}
|
|
/>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button color="primary" onClick={() => setShowAllowAction(false)}>
|
|
Close
|
|
</Button>
|
|
<Button
|
|
color="primary"
|
|
disabled={
|
|
!selectedAction || !actions.includes(selectedAction ? selectedAction.label : "")
|
|
}
|
|
onClick={allow}>
|
|
Allow
|
|
</Button>
|
|
</DialogActions>
|
|
</ResponsiveDialog>
|
|
<ResponsiveDialog
|
|
maxWidth="sm"
|
|
fullWidth
|
|
open={showEnableFeature}
|
|
onClose={() => setShowEnableFeature(false)}
|
|
aria-labelledby="enable-feature">
|
|
<DialogTitle id="enable-feature-title">Enable a feature</DialogTitle>
|
|
<DialogContent>
|
|
<SearchSelect
|
|
label="Select a feature to enable"
|
|
selected={selectedFeature}
|
|
options={features
|
|
.filter(a => !selectedUser.settings.features.includes(a))
|
|
.map(a => {
|
|
return { value: a, label: a };
|
|
})}
|
|
changeSelection={(option: Option | null) =>
|
|
setSelectedFeature(option ? option : undefined)
|
|
}
|
|
/>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button color="primary" onClick={() => setShowEnableFeature(false)}>
|
|
Close
|
|
</Button>
|
|
<Button
|
|
color="primary"
|
|
disabled={
|
|
!selectedFeature || !features.includes(selectedFeature ? selectedFeature.label : "")
|
|
}
|
|
onClick={enable}>
|
|
Enable
|
|
</Button>
|
|
</DialogActions>
|
|
</ResponsiveDialog>
|
|
</PageContainer>
|
|
);
|
|
}
|