frontend/app/ProjectDeliverables/VidispineJobProgress.tsx (225 lines of code) (raw):

import React, { useEffect, useRef, useState, useCallback } from "react"; import axios from "axios"; import { VidispineJob } from "../vidispine/job/VidispineJob"; import { VError } from "ts-interface-checker"; import { Grid, IconButton, LinearProgress, Typography, } from "@material-ui/core"; import ErrorIcon from "@material-ui/icons/Error"; import InfoIcon from "@material-ui/icons/Info"; import CheckCircleIcon from "@material-ui/icons/CheckCircle"; import LaunchIcon from "@material-ui/icons/Launch"; import { makeStyles } from "@material-ui/core/styles"; import { getUnixTime, parseISO } from "date-fns"; interface VidispineJobProgressProps { jobId: string; vidispineBaseUrl: string; openJob: (jobID: string) => void; onRecordNeedsUpdate: () => void; modifiedDateTime: string; status: string; } const useStyles = makeStyles({}); const VidispineJobProgress: React.FC<VidispineJobProgressProps> = (props) => { const [jobData, setJobData] = useState<VidispineJob | undefined>(undefined); const [updateTimer, setUpdateTimer] = useState<number>(Date.now()); const [lastError, setLastError] = useState<string | undefined>(undefined); //we need to use a reference so that the timer callback can get access to the job data const jobDataRef = useRef<VidispineJob>(); jobDataRef.current = jobData; const [totalProgressWithinStep, setTotalProgressWithinStep] = useState< number | undefined >(0); const [totalStepProgress, setTotalStepProgress] = useState< number | undefined >(0); const [indeterminate, setIndeterminate] = useState<boolean>(true); const classes = useStyles(); /** * load in data for the job */ const loadJobData = async (initialMount = false) => { const aWeekAgo = getUnixTime(Date.now() - 604800000); const modDateTime = getUnixTime(parseISO(props.modifiedDateTime)); try { const response = await axios.get( `${props.vidispineBaseUrl}/API/job/${props.jobId}` ); const jobInfo = new VidispineJob(response.data); setIndeterminate(jobInfo.data.totalSteps <= 0); let overallProgress = jobInfo.data.currentStep && jobInfo.data.currentStep.number && jobInfo.data.totalSteps > 0 ? ((jobInfo.data.currentStep.number + 1) / jobInfo.data.totalSteps) * 100 : undefined; if (overallProgress) setTotalStepProgress(overallProgress); let subprogress: number = 0; if (jobInfo.data.currentStep && jobInfo.data.currentStep.progress) { const progressData = jobInfo.data.currentStep.progress; if (progressData.unit === "percent") { subprogress = progressData.value / 100; } else if (progressData.total) { subprogress = progressData.value / progressData.total; } else { subprogress = 0; } } if (overallProgress) setTotalProgressWithinStep( overallProgress - 100 / jobInfo.data.totalSteps + 100 * (subprogress / jobInfo.data.totalSteps) ); setJobData(jobInfo); setLastError(undefined); //let the parent know when the job finishes, this triggers a reload of the row data if (!initialMount && jobInfo?.didFinish()) props.onRecordNeedsUpdate(); } catch (err) { if (err instanceof VError) { console.error( "Vidispine returned unexpected data for ", props.jobId, ": ", err ); setLastError("Did not understand response"); clearInterval(updateTimer); setUpdateTimer(Date.now()); } else if (err.response?.status == 404) { if (aWeekAgo < modDateTime) { console.error("Job not found: ", err); setLastError("Job not found"); } else { console.error("Job older than a week: ", err); } } else { console.error("Could not load data from Vidispine: ", err); setLastError("Vidispine not responding"); } } }; /** * called on an interval timer to update the job status, while it is */ const updateHandler = () => { const job = jobDataRef.current; if (!job) { console.log("no job data"); return; } if (props.status != "Ready") { loadJobData(); } else { return; } }; useEffect(() => { loadJobData(true); return () => { console.log("clearing update timer for ", props.jobId); if (updateTimer) clearInterval(updateTimer); }; }, [props.status]); useEffect(() => { const interval = setInterval(() => setUpdateTimer(Date.now()), 5000); return () => { updateHandler(); clearInterval(interval); }; }, [props.jobId]); return ( <Grid container direction="column" spacing={3} id={`vs-job-${props.jobId}`}> <Grid item> {jobData?.wasSuccess() || (jobData?.data.currentStep?.description && !jobData?.didFinish()) || jobData?.getMetadata("errorMessage") || lastError ? ( <LinearProgress classes={classes} variant="buffer" value={totalProgressWithinStep} valueBuffer={totalStepProgress} /> ) : null} </Grid> <Grid item className="job-progress-caption"> <Grid container direction="row" alignItems="center" justifyContent="space-between" > {jobData?.wasSuccess() ? ( <> <Grid item> <CheckCircleIcon fontSize="small" style={{ color: "green" }} /> </Grid> <Grid item> <Typography variant="caption" id={`vs-job-${props.jobId}-completed`} > Completed </Typography> </Grid> </> ) : null} {jobData?.data.currentStep?.description && !jobData?.didFinish() ? ( <> <Grid item> <InfoIcon fontSize="small" style={{ color: "gray" }} /> </Grid> <Grid item> <Typography variant="caption" id={`vs-job-${props.jobId}-info`}> {jobData.data.currentStep.description} </Typography> </Grid> </> ) : null} {jobData?.getMetadata("errorMessage") || lastError ? ( <> <Grid item> <ErrorIcon fontSize="small" style={{ color: "red" }} /> </Grid> <Grid item> {jobData?.getMetadata("errorMessage") ? ( <Typography variant="caption" style={{ color: "red" }} id={`vs-job-${props.jobId}-joberr`} > {jobData.getMetadata("errorMessage")} </Typography> ) : null} {lastError ? ( <Typography variant="caption" style={{ color: "red" }} id={`vs-job-${props.jobId}-servererr`} > {lastError} </Typography> ) : null} </Grid> </> ) : null} <Grid item> {jobData?.wasSuccess() || (jobData?.data.currentStep?.description && !jobData?.didFinish()) || jobData?.getMetadata("errorMessage") || lastError ? ( <IconButton aria-label="expand row" size="small" onClick={() => { props.openJob(props.jobId); }} > <LaunchIcon /> </IconButton> ) : null} </Grid> </Grid> </Grid> </Grid> ); }; export default React.memo(VidispineJobProgress);