ui/job-view/details/tabs/SimilarJobsTab.jsx (307 lines of code) (raw):

import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faSpinner } from '@fortawesome/free-solid-svg-icons'; import { Button } from 'reactstrap'; import { thMaxPushFetchSize } from '../../../helpers/constants'; import { toDateStr, toShortDateStr } from '../../../helpers/display'; import { addAggregateFields, getBtnClass } from '../../../helpers/job'; import { getJobsUrl, textLogErrorsEndpoint } from '../../../helpers/url'; import JobModel from '../../../models/job'; import PushModel from '../../../models/push'; import { notify } from '../../redux/stores/notifications'; import { getProjectJobUrl } from '../../../helpers/location'; import { getData } from '../../../helpers/http'; class SimilarJobsTab extends React.Component { constructor(props) { super(props); this.pageSize = 20; this.state = { similarJobs: [], filterNoSuccessfulJobs: false, page: 1, selectedSimilarJob: null, hasNextPage: false, isLoading: true, }; } componentDidMount() { this.getSimilarJobs(); } getSimilarJobs = async () => { const { page, similarJobs, selectedSimilarJob } = this.state; const { repoName, selectedJobFull, notify } = this.props; const options = { // get one extra to detect if there are more jobs that can be loaded (hasNextPage) count: this.pageSize + 1, offset: (page - 1) * this.pageSize, }; if (this.state.filterNoSuccessfulJobs) { options.nosuccess = ''; } const { data: newSimilarJobs, failureStatus, } = await JobModel.getSimilarJobs(selectedJobFull.id, options); if (!failureStatus) { this.setState({ hasNextPage: newSimilarJobs.length > this.pageSize }); if (this.state.hasNextPage) { /* The request fetches one task more than desired to check if there are more similar tasks. This last similar task doesn't get rendered for this result list but only if one requests more similar jobs. */ newSimilarJobs.pop(); } // create an array of unique push ids const pushIds = [...new Set(newSimilarJobs.map((job) => job.push_id))]; // get pushes and revisions for the given ids let pushList = { results: [] }; const { data, failureStatus } = await PushModel.getList({ id__in: pushIds.join(','), count: thMaxPushFetchSize, }); if (!failureStatus) { pushList = data; // decorate the list of jobs with their result sets const pushes = pushList.results.reduce( (acc, push) => ({ ...acc, [push.id]: push }), {}, ); newSimilarJobs.forEach((simJob) => { simJob.result_set = pushes[simJob.push_id]; simJob.revisionResultsetFilterUrl = getJobsUrl({ repo: repoName, revision: simJob.result_set.revisions[0].revision, }); simJob.authorResultsetFilterUrl = getJobsUrl({ repo: repoName, author: simJob.result_set.author, }); }); this.setState({ similarJobs: [...similarJobs, ...newSimilarJobs] }); // on the first page show the first element info by default if (!selectedSimilarJob && newSimilarJobs.length > 0) { this.showJobInfo(newSimilarJobs[0]); } } else { notify(`Error fetching similar jobs push data: ${data}`, 'danger', { sticky: true, }); } } else { notify(`Error fetching similar jobs: ${failureStatus}`, 'danger', { sticky: true, }); } this.setState({ isLoading: false }); }; // this is triggered by the show previous jobs button showNext = () => { const { page } = this.state; this.setState({ page: page + 1, isLoading: true }, this.getSimilarJobs); }; showJobInfo = (job) => { const { repoName, classificationMap } = this.props; JobModel.get(repoName, job.id).then(async (nextJob) => { addAggregateFields(nextJob); nextJob.failure_classification = classificationMap[nextJob.failure_classification_id]; // retrieve the list of error lines const { data, failureStatus } = await getData( getProjectJobUrl(textLogErrorsEndpoint, nextJob.id), ); if (!failureStatus && data.length) { nextJob.error_lines = data; } this.setState({ selectedSimilarJob: nextJob }); }); }; toggleFilter = (filterField) => { this.setState( (prevState) => ({ [filterField]: !prevState[filterField], similarJobs: [], isLoading: true, }), this.getSimilarJobs, ); }; render() { const { similarJobs, selectedSimilarJob, hasNextPage, filterNoSuccessfulJobs, isLoading, } = this.state; const selectedSimilarJobId = selectedSimilarJob ? selectedSimilarJob.id : null; return ( <div className="similar-jobs w-100" role="region" aria-label="Similar Jobs" > <div className="similar-job-list"> <table className="table table-super-condensed table-hover"> <thead> <tr> <th>Job</th> <th>Pushed</th> <th>Author</th> <th>Duration</th> <th>Revision</th> </tr> </thead> <tbody> {similarJobs.map((similarJob) => ( <tr key={similarJob.id} onClick={() => this.showJobInfo(similarJob)} className={ selectedSimilarJobId === similarJob.id ? 'table-active' : '' } > <td> <button className={`btn btn-similar-jobs btn-xs ${getBtnClass( similarJob.resultStatus, similarJob.failure_classification_id, )}`} type="button" > {similarJob.job_type_symbol} {similarJob.failure_classification_id > 1 && similarJob.failure_classification_id !== 6 && ( <span>*</span> )} </button> </td> <td title={toDateStr(similarJob.result_set.push_timestamp)}> {toShortDateStr(similarJob.result_set.push_timestamp)} </td> <td> <a href={similarJob.authorResultsetFilterUrl}> {similarJob.result_set.author} </a> </td> <td>{similarJob.duration} min</td> <td> <a href={similarJob.revisionResultsetFilterUrl}> {similarJob.result_set.revisions[0].revision} </a> </td> </tr> ))} </tbody> </table> {hasNextPage && ( <Button outline className="bg-light" type="button" onClick={this.showNext} > Show previous jobs </Button> )} </div> <div className="similar-job-detail-panel"> <form className="form form-inline"> <div className="checkbox"> <input onChange={() => this.toggleFilter('filterNoSuccessfulJobs')} type="checkbox" checked={filterNoSuccessfulJobs} /> <small>Exclude successful jobs</small> </div> </form> <div className="similar_job_detail"> {selectedSimilarJob && ( <table className="table table-super-condensed"> <tbody> <tr> <th>Result</th> <td>{selectedSimilarJob.resultStatus}</td> </tr> <tr> <th>Build</th> <td> {selectedSimilarJob.build_architecture}{' '} {selectedSimilarJob.build_platform}{' '} {selectedSimilarJob.build_os} </td> </tr> <tr> <th>Build option</th> <td>{selectedSimilarJob.platform_option}</td> </tr> <tr> <th>Job name</th> <td>{selectedSimilarJob.job_type_name}</td> </tr> <tr> <th>Started</th> <td>{toDateStr(selectedSimilarJob.start_timestamp)}</td> </tr> <tr> <th>Duration</th> <td> {selectedSimilarJob.duration >= 0 ? `${selectedSimilarJob.duration.toFixed(0)} minute(s)` : 'unknown'} </td> </tr> <tr> <th>Classification</th> <td> <strong className={`badge ${selectedSimilarJob.failure_classification.star}`} > {selectedSimilarJob.failure_classification.name} </strong> </td> </tr> {!!selectedSimilarJob.error_lines && ( <tr> <td colSpan={2}> <ul className="list-unstyled error_list"> {selectedSimilarJob.error_lines.map((error) => ( <li key={error.id}> <small title={error.line}>{error.line}</small> </li> ))} </ul> </td> </tr> )} </tbody> </table> )} </div> </div> {isLoading && ( <div className="overlay"> <div> <FontAwesomeIcon icon={faSpinner} pulse className="th-spinner-lg" title="Loading..." /> </div> </div> )} </div> ); } } SimilarJobsTab.propTypes = { repoName: PropTypes.string.isRequired, classificationMap: PropTypes.shape({}).isRequired, notify: PropTypes.func.isRequired, selectedJobFull: PropTypes.shape({}).isRequired, }; export default connect(null, { notify })(SimilarJobsTab);