frontend/app/JobsList/JobsList.jsx (317 lines of code) (raw):
import React from 'react';
import axios from 'axios';
import {formatError} from '../common/ErrorViewComponent.jsx';
import omit from "lodash.omit";
import JobsFilterComponent from "./JobsFilterComponent.jsx";
import {makeJobsListColumns} from "./JobsListContent";
import AdminContainer from "../admin/AdminContainer";
import {DataGrid} from "@material-ui/data-grid";
import {withStyles, createStyles, Paper, Dialog, DialogTitle, Typography, Snackbar, Theme} from "@material-ui/core";
import MuiAlert from "@material-ui/lab/Alert";
import uuid from "uuid";
import {Helmet} from "react-helmet";
const styles = (theme) => createStyles({
tableContainer: {
marginTop: "1em",
height: "80vh"
},
logLine: {
fontFamily: "Courier, serif",
color: theme.palette.success.dark,
},
silentList: {
listType: "none"
}
});
class JobsList extends React.Component {
static knownKeys = [
"jobType",
"jobStatus",
"sourceId"
]; //we will respond to these parameters on the query string as filters
constructor(props){
super(props);
this.state = {
jobsList: [],
loading: false,
showRelativeTimes: true,
showingAlert: false,
lastError: null,
showMessage: null,
activeFilters: {
jobType: "proxy"
},
showingLog: false,
logContent: "",
specificJob: null
};
this.filterUpdated = this.filterUpdated.bind(this);
this.filterbarUpdated = this.filterbarUpdated.bind(this);
this.handleModalClose = this.handleModalClose.bind(this);
this.refreshData = this.refreshData.bind(this);
this.closeAlert = this.closeAlert.bind(this);
this.openItemRequested = this.openItemRequested.bind(this);
this.openLog = this.openLog.bind(this);
this.subComponentErrored = this.subComponentErrored.bind(this);
this.resubmitSuccess = this.resubmitSuccess.bind(this);
}
/**
* updates the given job in the job list
* @param jobToUpdate
* @returns {Promise} promise that resolves once the update has completed
*/
updateJobList(jobToUpdate){
return new Promise((resolve,reject)=>{
try {
const itemIndex = this.state.jobsList.findIndex(job => job.jobId === jobToUpdate.jobId);
this.setState({
jobsList: this.state.jobsList.slice(0, itemIndex).concat([jobToUpdate]).concat(this.state.jobsList.slice(itemIndex + 1))
}, () => resolve());
} catch(ex){
reject(ex);
}
})
}
/**
* ask the server to update the job info about the given job. Currently only works for transcode jobs.
* @param jobId
*/
refreshJob(jobId){
this.setState({loading: true}, ()=>axios.put("/api/job/transcode/" + jobId + "/refresh").then(response=>{
console.log("Job update request worked");
window.setTimeout(()=>{
axios.get("/api/job/" + jobId).then(response=>{
this.updateJobList(response.data.entry).then(()=>{
this.setState({loading: false});
}).catch(err=>{
this.setState({loading:false, lastError: err, showingAlert: true});
})
})
}, 1000)
}).catch(err=>{
console.error(err);
axios.get("/api/job/" + jobId).then(response=>{
this.updateJobList(response.data.entry).then(()=>{
this.setState({loading: false});
}).catch(err=>{
this.setState({loading:false, lastError: err, showingAlert: true});
})
});
}))
}
componentDidCatch(error, errorInfo) {
console.error("JobsList failed to load: ", error, errorInfo);
}
static getDerivedStateFromError(err) {
return {
lastError: "A frontend internal error occurred, please see the browser console for more details",
showingAlert: true,
loading: false,
}
}
/**
* called by FilterButton when the filter status changes
* @param fieldName field to updated- provided when setting up the filter button
* @param values value to add/remove - provided when setting up the filter button
* @param type whether to add (plus) or remove (minus)
*/
filterUpdated(fieldName, values, type){
switch (type) {
case "plus":
let toUpdate = {};
toUpdate[fieldName] = values;
this.setState({
activeFilters: Object.assign({}, this.state.activeFilters, toUpdate)
}, ()=>{
this.props.history.push(this.queryParamsFromFilters());
this.refreshData()
});
break;
case "minus":
this.setState({
activeFilters: omit(this.state.activeFilters, fieldName)
}, ()=>{
this.props.history.push(this.queryParamsFromFilters());
this.refreshData()
});
break;
default:
console.error("expected plus or minus in filterUpdate, got ", type);
}
}
filterbarUpdated(newFilters){
console.log("Filter bar updated: ", newFilters);
this.setState({activeFilters: newFilters}, ()=>{
this.props.history.push(this.queryParamsFromFilters());
this.refreshData();
});
}
makeUpdateRequest(){
return Object.keys(this.state.activeFilters).length === 0 ?
axios.get("/api/job/all") : axios.put("/api/job/search",this.state.activeFilters)
}
/**
* MUI wants an 'id' prop on each record, so let's give it one
*/
enrichServerEntry(sourceData) {
if(sourceData.hasOwnProperty("jobId")) {
return Object.assign(sourceData, {id: sourceData.jobId})
} else {
return Object.assign(sourceData, {id: uuid()});
}
}
refreshData(){
if(this.state.specificJob){
this.setState({lastError: null, loading: true}, ()=>axios.get("/api/job/" + this.state.specificJob).then(result=>
this.setState({lastError: null, jobsList: [this.enrichServerEntry(result.data.entry)], loading: false})
).catch(err=>{
console.error(err);
this.setState({lastError: err, loading: false, showingAlert: true});
}))
} else {
this.setState({lastError: null, loading: true}, () => this.makeUpdateRequest().then(result => {
this.setState({lastError: null, jobsList: result.data.entries.map(this.enrichServerEntry), loading: false})
}).catch(err => {
console.error(err);
this.setState({loading: false, lastError: err, showingAlert: true});
}))
}
}
componentDidUpdate(oldProps, oldState, snapshot){
/*if the url changes, update the data */
if(oldProps.match!==this.props.match){
this.setState({specificJob: this.props.match.params.hasOwnProperty("jobid") ? this.props.match.params.jobid : null},()=>
this.refreshData());
}
if(oldState.lastError==null && this.state.lastError!=null) {
this.setState({
showingMessage: null
});
}
if(oldState.showingMessage==null && this.state.showingMessage!=null) {
this.setState({
lastError: null
})
}
}
/**
* converts the current status of filters dict to a query string
*/
queryParamsFromFilters(){
return "?" + Object.keys(this.state.activeFilters).map(key=>key + "=" + this.state.activeFilters[key]).join("&");
}
/**
* parses an available query string and extracts relevant filters for intial page state
*/
filtersFromQueryParams(){
const parts = this.props.location.search.split('&');
console.log(parts);
const breakdown = parts.reduce((acc,entry)=>{
const kv = entry.split('=');
const key = kv[0][0] === '?' ? kv[0].substr(1) : kv[0];
acc[key] = kv[1];
return acc;
}, {});
console.log(breakdown);
return Object.keys(breakdown)
.filter(key=>JobsList.knownKeys.includes(key))
.reduce((acc, key)=>{
acc[key]=breakdown[key];
return acc;
}, {});
}
componentDidMount(){
const qpFilters = this.filtersFromQueryParams();
console.log(qpFilters);
const initialFilters = qpFilters.length===0 ? this.state.activeFilters : qpFilters;
this.setState(
{
jobsList:[],
specificJob: this.props.match.params.hasOwnProperty("jobid") ? this.props.match.params.jobid : null,
activeFilters: initialFilters
},
()=>this.refreshData()
);
}
handleModalClose(){
this.setState({showingLog: false});
}
closeAlert() {
this.setState({showingAlert: false});
}
openItemRequested(itemRecord) {
let url = undefined;
switch(itemRecord.sourceType) {
case "SRC_MEDIA":
url = `/browse?open=${encodeURIComponent(itemRecord.sourceId)}`;
break;
case "SRC_SCANTARGET":
url = `/admin/scanTargets/${encodeURIComponent(itemRecord.sourceId)}`;
break;
default:
break;
}
if(url) this.props.history.push(url);
}
openLog() {
this.setState({showingLog: true});
}
subComponentErrored(errorDesc) {
this.setState({
lastError: errorDesc,
showingAlert: true
});
}
resubmitSuccess() {
this.setState({
showMessage: "Job was resubmitted",
showingAlert: true
});
}
render(){
const columns = makeJobsListColumns(this.filterUpdated,
this.openLog,
this.openItemRequested,
this.subComponentErrored,
this.resubmitSuccess,
this.state.showRelativeTimes);
return <AdminContainer {...this.props}>
<Helmet>
<title>Jobs - ArchiveHunter</title>
</Helmet>
<Snackbar open={this.state.showingAlert} onClose={this.closeAlert} autoHideDuration={8000}>
<>
{
this.state.lastError ? <MuiAlert severity="error" onClose={this.closeAlert}>
{typeof(this.state.lastError)==="string" ? this.state.lastError : formatError(this.state.lastError, false)}
</MuiAlert> : null
}
{
this.state.showMessage ? <MuiAlert severity="info" onClose={this.closeAlert}>
{this.state.showMessage}
</MuiAlert> : null
}
</>
</Snackbar>
<JobsFilterComponent activeFilters={this.state.activeFilters}
filterChanged={this.filterbarUpdated}
refreshClicked={this.refreshData}
isLoading={this.state.loading}
/>
<Dialog open={this.state.showingLog} onClose={()=>this.setState({showingLog: false})} aria-labelledby="logs-title">
<DialogTitle id="logs-title">Logs</DialogTitle>
<ul className={this.props.classes.silentList}>
{
this.state.logContent
.split("\n")
.map((line, idx)=><li key={idx}><Typography className={this.props.classes.logLine}>{line}</Typography></li>)
}
</ul>
</Dialog>
<Paper elevation={3} className={this.props.classes.tableContainer}>
<DataGrid columns={columns} rows={this.state.jobsList}/>
</Paper>
</AdminContainer>
}
}
export default withStyles(styles)(JobsList);