frontend/app/ProjectRecordValidation/ValidationJobResults.tsx (261 lines of code) (raw):

import React, { useEffect, useState } from "react"; import { useParams } from "react-router-dom"; import axios from "axios"; import { Grid, IconButton, LinearProgress, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TablePagination, TableRow, TableSortLabel, Typography, } from "@material-ui/core"; import { Alert } from "@material-ui/lab"; import { differenceInMinutes, format, parseISO } from "date-fns"; import ValidationTableRow from "./ValidationTableRow"; import { useHistory } from "react-router"; import { ArrowBackRounded } from "@material-ui/icons"; import { Helmet } from "react-helmet"; import { useGuardianStyles } from "~/misc/utils"; interface ValidationJobResultsLocationParams { jobId: string; } type SortColumns = "job-id" | "item-id" | "detection-time"; type SortOrders = "asc" | "desc"; const ValidationJobResults: React.FC = () => { const [loading, setLoading] = useState(false); const [totalProblemReports, setTotalProblemReports] = useState(0); const [problemReports, setProblemReports] = useState<ValidationProblem[]>([]); const [jobDetails, setJobDetails] = useState<ValidationJob | undefined>( undefined ); const [lastError, setLastError] = useState<string | undefined>(undefined); const [currentPageNumber, setCurrentPageNumber] = useState(0); const [currentRowsPerPage, setCurrentRowsPerPage] = useState(10); const rowsPerPageOptions = [10, 25, 50, 75, 100]; const [sortColumn, setSortColumn] = useState<SortColumns>("detection-time"); const [sortOrder, setSortOrder] = useState<SortOrders>("desc"); const routerParams = useParams<ValidationJobResultsLocationParams>(); const history = useHistory(); const classes = useGuardianStyles(); const changeRowsPerPage = ( evt: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement> ) => { setCurrentRowsPerPage(parseInt(evt.target.value, 10)); setCurrentPageNumber(0); }; const tablePageChanged = ( evt: React.MouseEvent<HTMLButtonElement> | null, page: number ) => { setCurrentPageNumber(page); }; useEffect(() => { refreshData(); }, [currentPageNumber, currentRowsPerPage, sortColumn, sortOrder]); const refreshData = async () => { setLoading(true); try { const jobDetailsResponse = await axios.get<ValidationJob>( `/api/validation/${routerParams.jobId}`, { validateStatus: (status) => status === 200 || status === 404 } ); if (jobDetailsResponse.status === 404) { setLastError("There is no job with this ID"); setLoading(false); } else { setJobDetails(jobDetailsResponse.data); const startAt = currentPageNumber * currentRowsPerPage; const response = await axios.get<ValidationProblemListResponse>( `/api/validation/${routerParams.jobId}/faults?from=${startAt}&limit=${currentRowsPerPage}&sortColumn=${sortColumn}&sortOrder=${sortOrder}` ); setLastError(undefined); setProblemReports(response.data.entries); setLoading(false); setTotalProblemReports(response.data.totalCount); } } catch (err) { console.error(err); setLastError(err.toString()); setLoading(false); } }; /** * returns a UI string representing the time that the job has been in progress */ const getElapsedTime = () => { const maybeStartTime = jobDetails?.startedAt ? parseISO(jobDetails.startedAt) : undefined; const maybeCompletedTime = jobDetails?.completedAt ? parseISO(jobDetails.completedAt) : undefined; try { if (maybeStartTime && maybeCompletedTime) { const duration = differenceInMinutes( maybeCompletedTime, maybeStartTime ); if (duration > 60) { const hrs = Math.floor(duration); const remainingMins = Math.floor(duration - hrs * 60); return `Completed in ${hrs} hours and ${remainingMins} mins`; } else { return `Completed in ${duration} minutes`; } } else if (maybeStartTime) { const duration = differenceInMinutes(Date.now(), maybeStartTime); if (duration > 60) { const hrs = Math.floor(duration); const remainingMins = Math.floor(duration - hrs * 60); return `Running for ${hrs} hours and ${remainingMins} mins`; } else { return `Running for ${duration} minutes`; } } else { return "Not started yet"; } } catch (err) { console.error("Could not parse times from ", jobDetails); console.error("Error was ", err); return "Invalid data"; } }; const formatStartTime = () => { if (jobDetails?.startedAt) { try { const parsedTime = parseISO(jobDetails.startedAt); return format(parsedTime, "E do MMM yyyy HH:mm:ss xx"); } catch (err) { console.error( "Could not parse and format ", jobDetails.startedAt, ": ", err ); return "Invalid data"; } } else { return "Not started yet"; } }; const columnClicked = (col: SortColumns) => { setSortColumn((prevSortColumn) => { if (prevSortColumn === col) { //if we are clicking on the column already selected, change the sort order setSortOrder((prevState) => (prevState === "asc" ? "desc" : "asc")); return col; } else { //otherwise change the column return col; } }); }; return ( <> <Helmet> <title>Project validation results</title> </Helmet> {loading ? <LinearProgress style={{ width: "100%" }} /> : undefined} <Typography variant="h2">Validate project files</Typography> {lastError ? <Alert severity="error">{lastError}</Alert> : undefined} <Paper elevation={3}> {jobDetails ? ( <Grid container spacing={5} className={classes.infoBanner}> <Grid item> <IconButton onClick={() => history.push("/validate/project")}> <ArrowBackRounded /> </IconButton> </Grid> <Grid item> <Typography className={classes.headerTitle}>Job ID</Typography> <Typography>{jobDetails.uuid}</Typography> </Grid> <Grid item> <Typography className={classes.headerTitle}>Scan type</Typography> <Typography>{jobDetails.jobType}</Typography> </Grid> <Grid item> <Typography className={classes.headerTitle}>Run by</Typography> <Typography>{jobDetails.userName}</Typography> </Grid> <Grid item> <Typography className={classes.headerTitle}> Current status </Typography> <Typography>{jobDetails.status}</Typography> </Grid> <Grid item> <Typography className={classes.headerTitle}> Started at </Typography> <Typography>{formatStartTime()}</Typography> </Grid> <Grid item> <Typography className={classes.headerTitle}> Elapsed time </Typography> <Typography>{getElapsedTime()}</Typography> </Grid> <Grid item> <Typography className={classes.headerTitle}> Total faults found </Typography> <Typography>{totalProblemReports}</Typography> </Grid> </Grid> ) : undefined} </Paper> <Paper elevation={3}> <TableContainer> <Table size="small" className={classes.resultsTable}> <TableHead> <TableRow> <TableCell> <TableSortLabel active={sortColumn === "detection-time"} direction={ sortColumn === "detection-time" ? sortOrder : "asc" } onClick={() => columnClicked("detection-time")} > Problem detected at </TableSortLabel> </TableCell> <TableCell> <TableSortLabel active={sortColumn === "item-id"} direction={sortColumn === "item-id" ? sortOrder : "asc"} onClick={() => columnClicked("item-id")} > Affected item </TableSortLabel> </TableCell> <TableCell> <Typography>Details</Typography> </TableCell> </TableRow> </TableHead> <TableBody> {problemReports.map((entry, idx) => ( <ValidationTableRow data={entry} key={idx} /> ))} </TableBody> </Table> </TableContainer> <TablePagination count={totalProblemReports} component="div" classes={{ root: classes.fullWidth }} page={currentPageNumber} onPageChange={tablePageChanged} rowsPerPage={currentRowsPerPage} onRowsPerPageChange={changeRowsPerPage} rowsPerPageOptions={rowsPerPageOptions} /> </Paper> </> ); }; export default ValidationJobResults;