import DeleteIcon from "@mui/icons-material/Delete"; import DownloadIcon from "@mui/icons-material/Download"; import MenuBookIcon from "@mui/icons-material/MenuBook"; import RefreshIcon from "@mui/icons-material/Refresh"; import { Alert, Button, CircularProgress, IconButton, LinearProgress, TextField, Tooltip, Typography, } from "@mui/material"; import { DataGrid, GridColDef } from "@mui/x-data-grid"; import { filesize } from "filesize"; import React from "react"; import { IsoFile, IsoFilesApi } from "../api/IsoFilesApi"; import { ServerApi } from "../api/ServerApi"; import { IsoCatalogDialog } from "../dialogs/IsoCatalogDialog"; import { useAlert } from "../hooks/providers/AlertDialogProvider"; import { useConfirm } from "../hooks/providers/ConfirmDialogProvider"; import { useLoadingMessage } from "../hooks/providers/LoadingMessageProvider"; import { useSnackbar } from "../hooks/providers/SnackbarProvider"; import { downloadBlob } from "../utils/FilesUtils"; import { AsyncWidget } from "../widgets/AsyncWidget"; import { FileInput } from "../widgets/forms/FileInput"; import { VirtWebPaper } from "../widgets/VirtWebPaper"; import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; export function IsoFilesRoute(): React.ReactElement { const [list, setList] = React.useState<IsoFile[] | undefined>(); const [isoCatalog, setIsoCatalog] = React.useState(false); const loadKey = React.useRef(1); const load = async () => { setList(await IsoFilesApi.GetList()); }; const reload = () => { loadKey.current += 1; setList(undefined); }; return ( <> <AsyncWidget loadKey={loadKey.current} errMsg="Failed to load ISO files list!" load={load} ready={list !== undefined} build={() => ( <VirtWebRouteContainer label="ISO files management" actions={ <span> <Tooltip title="Open the ISO catalog"> <IconButton onClick={() => { setIsoCatalog(true); }}> <MenuBookIcon /> </IconButton> </Tooltip> <Tooltip title="Refresh ISO list"> <IconButton onClick={reload}> <RefreshIcon /> </IconButton> </Tooltip> </span> } > <UploadIsoFileCard onFileUploaded={reload} /> <UploadIsoFileFromUrlCard onFileUploaded={reload} /> <IsoFilesList list={list!} onReload={reload} /> </VirtWebRouteContainer> )} /> <IsoCatalogDialog open={isoCatalog} onClose={() => { setIsoCatalog(false); }} /> </> ); } function UploadIsoFileCard(p: { onFileUploaded: () => void; }): React.ReactElement { const alert = useAlert(); const snackbar = useSnackbar(); const [value, setValue] = React.useState<File | null>(null); const [uploadProgress, setUploadProgress] = React.useState<number | null>( null ); const handleChange = (newValue: File | null) => { if (newValue && newValue.size > ServerApi.Config.constraints.iso_max_size) { alert( `The file is too big (max size allowed: ${filesize( ServerApi.Config.constraints.iso_max_size )}` ); return; } if (newValue && !ServerApi.Config.iso_mimetypes.includes(newValue.type)) { alert(`Selected file mimetype is not allowed! (${newValue.type})`); return; } setValue(newValue); }; const upload = async () => { try { setUploadProgress(0); await IsoFilesApi.Upload(value!, setUploadProgress); setValue(null); snackbar("The file was successfully uploaded!"); p.onFileUploaded(); } catch (e) { console.error(e); await alert(`Failed to perform file upload! ${e}`); } setUploadProgress(null); }; if (uploadProgress !== null) { return ( <VirtWebPaper label="File upload" noHorizontalMargin> <Typography variant="body1"> Upload in progress ({Math.floor(uploadProgress * 100)}%)... </Typography> <LinearProgress variant="determinate" value={uploadProgress * 100} /> </VirtWebPaper> ); } return ( <VirtWebPaper label="File upload" noHorizontalMargin> <div style={{ display: "flex", alignItems: "center" }}> <FileInput value={value} onChange={handleChange} style={{ flex: 1 }} slotProps={{ htmlInput: { accept: ServerApi.Config.iso_mimetypes.join(",") }, }} /> {value && <Button onClick={upload}>Upload file</Button>} </div> </VirtWebPaper> ); } function UploadIsoFileFromUrlCard(p: { onFileUploaded: () => void; }): React.ReactElement { const alert = useAlert(); const snackbar = useSnackbar(); const loadingMessage = useLoadingMessage(); const [url, setURL] = React.useState(""); const [filename, setFilename] = React.useState<null | string>(null); const autoFileName = url.split("/").slice(-1)[0]; const actualFileName = filename ?? autoFileName; const upload = async () => { try { loadingMessage.show("Downloading file from URL..."); await IsoFilesApi.UploadFromURL(url, actualFileName); p.onFileUploaded(); setURL(""); setFilename(null); snackbar("Successfully downloaded file!"); } catch (e) { console.error(e); alert("Failed to download file!"); } loadingMessage.hide(); }; return ( <VirtWebPaper label="File upload from URL" noHorizontalMargin> <div style={{ display: "flex", alignItems: "center" }}> <TextField label="URL" value={url} style={{ flex: 3 }} onChange={(e) => { setURL(e.target.value); }} /> <span style={{ width: "10px" }}></span> <TextField label="Filename" value={actualFileName} style={{ flex: 2 }} onChange={(e) => { setFilename(e.target.value); }} /> {url !== "" && actualFileName !== "" && ( <Button onClick={upload}>Upload file</Button> )} </div> </VirtWebPaper> ); } function IsoFilesList(p: { list: IsoFile[]; onReload: () => void; }): React.ReactElement { const confirm = useConfirm(); const alert = useAlert(); const loadingMessage = useLoadingMessage(); const snackbar = useSnackbar(); const [dlProgress, setDlProgress] = React.useState<undefined | number>(); const downloadIso = async (entry: IsoFile) => { setDlProgress(0); try { const blob = await IsoFilesApi.Download(entry, setDlProgress); downloadBlob(blob, entry.filename); } catch (e) { console.error(e); alert("Failed to download iso file!"); } setDlProgress(undefined); }; const deleteIso = async (entry: IsoFile) => { if ( !(await confirm( `Do you really want to delete this file (${entry.filename}) ?` )) ) return; loadingMessage.show("Deleting ISO file..."); try { await IsoFilesApi.Delete(entry); snackbar("The file has been successfully deleted!"); p.onReload(); } catch (e) { console.error(e); alert(`Failed to delete file!\n${e}`); } loadingMessage.hide(); }; if (p.list.length === 0) return ( <Typography variant="body1" style={{ textAlign: "center" }}> No ISO file uploaded for now. </Typography> ); const columns: GridColDef<IsoFile>[] = [ { field: "filename", headerName: "File name", flex: 3 }, { field: "size", headerName: "File size", flex: 1, renderCell(params) { return filesize(params.row.size); }, }, { field: "actions", headerName: "", width: 120, renderCell(params) { return ( <> <Tooltip title="Download file"> <IconButton onClick={() => downloadIso(params.row)}> <DownloadIcon /> </IconButton> </Tooltip> <Tooltip title="Delete file"> <IconButton onClick={() => deleteIso(params.row)}> <DeleteIcon /> </IconButton> </Tooltip> </> ); }, }, ]; return ( <> {/* Download notification */} {dlProgress !== undefined && ( <Alert severity="info"> <div style={{ display: "flex", flexDirection: "row", alignItems: "center", overflow: "hidden", }} > <Typography variant="body1"> Downloading... {dlProgress}% </Typography> <CircularProgress variant="determinate" size={"1.5rem"} style={{ marginLeft: "10px" }} value={dlProgress} /> </div> </Alert> )} {/* ISO files list table */} <DataGrid getRowId={(c) => c.filename} rows={p.list} columns={columns} /> </> ); }