frontend/src/pages/Firmware.tsx
2025-05-08 13:45:13 -06:00

482 lines
14 KiB
TypeScript

import {
Avatar,
Box,
IconButton,
ListItemAvatar,
ListItemText,
Menu,
MenuItem,
Theme,
Toolbar,
Tooltip,
TypeBackground
} from "@mui/material";
import { blue, green, orange, red } from "@mui/material/colors";
import { createStyles, makeStyles } from "@mui/styles";
//import { TypeBackground } from "@material-ui/core/styles/createPalette";
import {
ArrowUpward,
CloudDownload,
CloudUpload,
Delete,
MoreVert,
OpenInNew
} from "@mui/icons-material";
import classNames from "classnames";
import ResponsiveTable, { Column } from "common/ResponsiveTable";
import SmartBreadcrumb from "common/SmartBreadcrumb";
import UpdateFirmwareChannel from "firmware/UpdateFirmwareChannel";
import UploadFirmware from "firmware/UploadFirmware";
import { useFirmwareAPI, useSnackbar, useThemeType, useWidth } from "hooks";
//import MaterialTable, { Column, MTableToolbar } from "material-table";
import { Firmware as FirmwareModel } from "models";
import moment from "moment";
import PageContainer from "pages/PageContainer";
import { getDevicePlatformLabel, getDevicePlatformName } from "pbHelpers/DevicePlatform";
import { getUpgradeChannelColour, getUpgradeChannelLabel } from "pbHelpers/UpgradeChannel";
import { pond } from "protobuf-ts/pond";
import { useGlobalState } from "providers";
import { useCallback, useEffect, useState } from "react";
import { downloadFile } from "utils/download";
import { or } from "utils/types";
const useStyles = makeStyles((theme: Theme) => {
const themeType = useThemeType()
return ({
gutter: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(2),
[theme.breakpoints.up("sm")]: {
paddingLeft: theme.spacing(3),
paddingRight: theme.spacing(3)
}
},
actionsMenu: {
background:
themeType === "light"
? theme.palette.background["200" as keyof TypeBackground]
: theme.palette.background["700" as keyof TypeBackground]
},
avatar: {
margin: theme.spacing(1)
},
downloadIcon: {
color: theme.palette.text.primary,
backgroundColor: blue["500"],
"&:hover": {
backgroundColor: blue["600"]
}
},
updateIcon: {
color: theme.palette.text.primary,
backgroundColor: orange["500"],
"&:hover": {
backgroundColor: orange["600"]
}
},
changelogIcon: {
color: theme.palette.text.primary,
backgroundColor: green["500"],
"&:hover": {
backgroundColor: green["600"]
}
},
deleteIcon: {
color: theme.palette.text.primary,
backgroundColor: red["500"],
"&:hover": {
backgroundColor: red["600"]
}
},
menuItem: {
paddingTop: theme.spacing(2),
paddingBottom: theme.spacing(2)
},
title: {
flex: "0 0 auto"
},
toolbar:
themeType === "light"
? {
position: "sticky",
justifyContent: "space-between",
background: "#fff",
zIndex: theme.zIndex.mobileStepper,
top: "55px",
paddingRight: theme.spacing(1),
[theme.breakpoints.up("sm")]: {
paddingRight: theme.spacing(2),
top: "63px"
}
}
: {
position: "sticky",
justifyContent: "space-between",
background: theme.palette.background["800" as keyof TypeBackground],
zIndex: theme.zIndex.mobileStepper,
top: "55px",
paddingRight: theme.spacing(1),
[theme.breakpoints.up("sm")]: {
paddingRight: theme.spacing(2),
top: "63px"
}
},
toolbarActions: {
color: theme.palette.text.secondary
},
cellPadding: {
paddingLeft: theme.spacing(0.5) + "px",
paddingRight: theme.spacing(0.5) + "px",
"@media (min-width: 321px)": {
paddingLeft: theme.spacing(1) + "px",
paddingRight: theme.spacing(1) + "px"
},
[theme.breakpoints.up("sm")]: {
paddingLeft: theme.spacing(4) + "px",
paddingRight: theme.spacing(4) + "px"
}
},
pointer: {
cursor: "pointer"
},
tableHead:
themeType === "light"
? {
position: "sticky",
background: "#fff",
zIndex: theme.zIndex.mobileStepper,
top: "110px",
[theme.breakpoints.up("sm")]: {
top: "126px"
}
}
: {
position: "sticky",
background: theme.palette.background["800" as keyof TypeBackground],
zIndex: theme.zIndex.mobileStepper,
top: "110px",
[theme.breakpoints.up("sm")]: {
top: "126px"
}
},
noFirmware: {
marginTop: theme.spacing(16)
},
darkIcon: {
color: theme.palette.primary.dark
},
loading: {
display: "flex",
alignItems: "center",
justifyContent: "center",
marginTop: theme.spacing(16)
}
})
})
export default function Firmware() {
const classes = useStyles();
const width = useWidth();
const { error, success } = useSnackbar();
const firmwareAPI = useFirmwareAPI();
const [{ user }] = useGlobalState();
const [pageSize, setPageSize] = useState<number>(5);
const [tablePage, setTablePage] = useState(0)
const [searchText, setSearchText] = useState("")
const [tableData, setTableData] = useState<FirmwareModel[]>([])
const [total, setTotal] = useState(0)
const [selected, setSelected] = useState<FirmwareModel | undefined>(undefined);
const [actionsOpen, setActionsOpen] = useState<boolean>(false);
const [isUploadFirmwareDialogOpen, setIsUploadFirmwareDialogOpen] = useState<boolean>(false);
const [isUpdateFirmwareChannelDialogOpen, setIsUpdateFirmwareChannelDialogOpen] = useState<
boolean
>(false);
const [anchor, setAnchor] = useState<any>(null);
// const TitleMap = new Map([
// ["Uploaded", "uploaded"],
// ["Version", "version"],
// ["Platform", "platform"],
// ["Channel", "channel"],
// ["Size", "size"]
// ]);
// let tableRef = useRef<any>(React.createRef());
// const reloadTable = () => {
// tableRef.current.onQueryChange();
// };
const load = useCallback(() => {
firmwareAPI.listFirmware(pageSize, pageSize * tablePage, "desc", "version", searchText)
.then((response) => {
setTotal(response.data.total ? response.data.total : 0)
let rows: FirmwareModel[] = or(response.data.firmware, []).map(
(firmwareData: any) => {
return FirmwareModel.create(pond.Firmware.fromObject(firmwareData));
}
);
setTableData(rows)
})
.catch(() => {
//failed to load
error("Failed to load firmware versions")
});
},[pageSize, tablePage, searchText])
useEffect(()=>{
load()
},[load])
const download = () => {
if (selected) {
firmwareAPI
.downloadInstaller(selected.settings.platform, selected.settings.version)
.then((response: Response) => {
var filename: string =
selected.settings.version +
"-" +
getDevicePlatformName(selected.settings.platform) +
".tar";
downloadFile(response, filename);
})
.catch((err: any) => {
error(
err.response ? err.response.statusText : "Error occured while downloading firmware"
);
})
.then(() => closeActionsMenu());
}
};
const hasChangelog = () => {
return (
selected && selected.settings.channel !== pond.UpgradeChannel.UPGRADE_CHANNEL_DEVELOPMENT
);
};
const viewChangelog = () => {
if (selected && selected.settings.version) {
window.open(
"https://gitlab.com/brandx/gadwall/-/tags/" + selected.settings.version,
"_blank"
);
}
};
const deleteFirmware = () => {
if (selected) {
firmwareAPI
.removeFirmware(selected.settings.platform, selected.settings.version)
.then((response: any) => {
success("Successfully removed firmware");
})
.catch((err: any) => {
error(
err.response
? err.response.statusText
: "Error occured while delete the firmware release"
);
})
.then(() => {
closeActionsMenu();
load();
});
}
};
const openActionsMenu = (firmware: FirmwareModel) => (event: any) => {
if (user.settings.actions.includes("upload-firmware")) {
setActionsOpen(true);
setAnchor(event.currentTarget);
setSelected(firmware);
} else {
error("Missing upload-firmware action");
}
};
const closeActionsMenu = () => {
setActionsOpen(false);
setAnchor(null);
setSelected(undefined);
};
const openUploadFirmwareDialog = (event: any) => {
if (user.settings.actions.includes("upload-firmware")) {
setActionsOpen(false);
setAnchor(event.currentTarget);
setIsUploadFirmwareDialogOpen(true);
} else {
error("Missing upload-firmware action");
}
};
const closeUploadFirmwareDialog = () => {
setActionsOpen(false);
setAnchor(undefined);
setSelected(undefined);
setIsUploadFirmwareDialogOpen(false);
};
const openUpdateFirmwareChannelDialog = (event: any) => {
setActionsOpen(false);
setAnchor(event.currentTarget);
setIsUpdateFirmwareChannelDialogOpen(true);
};
const closeUpdateFirmwareChannelDialog = () => {
setActionsOpen(false);
setAnchor(undefined);
setSelected(undefined);
setIsUpdateFirmwareChannelDialogOpen(false);
};
const renderActionsMenu = () => {
return (
<Menu
id="actionsMenu"
anchorEl={anchor}
open={actionsOpen}
onClose={closeActionsMenu}
disableAutoFocusItem
MenuListProps={{ classes: { root: classes.actionsMenu } }}>
<MenuItem className={classes.menuItem} onClick={download}>
<ListItemAvatar>
<Avatar
className={classNames(classes.avatar, classes.downloadIcon)}
alt="Cloud Download">
<CloudDownload className={classes.downloadIcon} />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Download Installer" />
</MenuItem>
<MenuItem className={classes.menuItem} onClick={openUpdateFirmwareChannelDialog}>
<ListItemAvatar>
<Avatar className={classNames(classes.avatar, classes.updateIcon)} alt="Update">
<ArrowUpward className={classes.updateIcon} />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Update Channel" />
</MenuItem>
<MenuItem
disabled={!hasChangelog()}
className={classes.menuItem}
onClick={viewChangelog}>
<ListItemAvatar>
<Avatar className={classNames(classes.avatar, classes.changelogIcon)} alt="Open New">
<OpenInNew className={classes.changelogIcon} />
</Avatar>
</ListItemAvatar>
<ListItemText primary="View Changelog" />
</MenuItem>
<MenuItem className={classes.menuItem} onClick={deleteFirmware}>
<ListItemAvatar>
<Avatar className={classNames(classes.avatar, classes.deleteIcon)} alt="Delete">
<Delete className={classes.deleteIcon} />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Delete Version" />
</MenuItem>
</Menu>
);
};
const columns = (small: boolean): Column<FirmwareModel>[] => {
return [
{
title: "Uploaded",
hidden: small,
//defaultSort: "desc",
cellStyle: {padding: 2},
render: row => {
return <span>{moment(row.status.uploaded).fromNow()}</span>;
}
},
{
title: "Version",
cellStyle: {padding: 2},
render: row => <span>{row.settings.version}</span>
},
{
title: "Platform",
cellStyle: {padding: 2},
render: row => <span>{getDevicePlatformLabel(row.settings.platform)}</span>
},
{
title: "Channel",
cellStyle: {padding: 2},
render: row => (
<span style={{ color: getUpgradeChannelColour(row.settings.channel) }}>
{getUpgradeChannelLabel(row.settings.channel)}
</span>
)
},
{
title: "Size",
cellStyle: {padding: 2},
//type: "numeric",
hidden: small,
render: row => <span>{row.settings.size} bytes</span>
},
{
title: "Actions",
cellStyle: {padding: 2},
//type: "numeric",
//sorting: false,
render: row => (
<IconButton aria-label="Actions" onClick={openActionsMenu(row)}>
<MoreVert fontSize={small ? "small" : "inherit"} />
</IconButton>
)
}
];
};
const actionToolbar = () => {
return (
<Toolbar className={classes.gutter}>
{user.allowedTo("upload-firmware") && (
<Tooltip title="Upload Firmware" placement="bottom">
<span>
<IconButton aria-label="Upload Firmware" onClick={openUploadFirmwareDialog}>
<CloudUpload
fontSize={width === "xs" ? "small" : "inherit"}
className={classes.darkIcon}
/>
</IconButton>
</span>
</Tooltip>
)}
</Toolbar>
);
};
return (
<PageContainer>
<Box margin={2}>
<ResponsiveTable<FirmwareModel>
subtitle={actionToolbar()}
setSearchText={(search) => setSearchText(search)}
title={<SmartBreadcrumb />}
page={tablePage}
pageSize={pageSize}
rows={tableData}
total={total}
columns={columns(width === "xs" || width === "sm")}
setPage={(page)=>{setTablePage(page)}}
handleRowsPerPageChange={(e)=>{setPageSize(e.target.value)}}
/>
</Box>
<UploadFirmware
isOpen={isUploadFirmwareDialogOpen}
closeDialogCallback={closeUploadFirmwareDialog}
refreshCallback={() => load()}
/>
<UpdateFirmwareChannel
firmware={selected ? selected : FirmwareModel.create()}
isOpen={isUpdateFirmwareChannelDialogOpen}
closeCallback={closeUpdateFirmwareChannelDialog}
refreshCallback={() => load()}
/>
{renderActionsMenu()}
</PageContainer>
);
}