ui/logviewer/App.jsx (340 lines of code) (raw):

import React from 'react'; import { hot } from 'react-hot-loader/root'; import { LazyLog } from 'react-lazylog'; import isEqual from 'lodash/isEqual'; import { Collapse } from 'reactstrap'; import { getAllUrlParams, getUrlParam, setUrlParam, getProjectJobUrl, } from '../helpers/location'; import { scrollToLine } from '../helpers/utils'; import { isReftest } from '../helpers/job'; import { getJobsUrl, getReftestUrl, getArtifactsUrl, textLogErrorsEndpoint, } from '../helpers/url'; import { getData } from '../helpers/http'; import JobModel from '../models/job'; import PushModel from '../models/push'; import JobArtifacts from '../shared/JobArtifacts'; import JobInfo from '../shared/JobInfo'; import RepositoryModel from '../models/repository'; import { formatArtifacts, errorLinesCss } from '../helpers/display'; import Navigation from './Navigation'; import ErrorLines from './ErrorLines'; import '../css/lazylog-custom-styles.css'; import './logviewer.css'; const JOB_DETAILS_COLLAPSED = 'jobDetailsCollapsed'; const getUrlLineNumber = function getUrlLineNumber() { const lineNumberParam = getUrlParam('lineNumber'); if (lineNumberParam) { return lineNumberParam.split('-').map((line) => parseInt(line, 10)); } return null; }; class App extends React.PureComponent { constructor(props) { super(props); const queryString = getAllUrlParams(); const collapseDetails = localStorage.getItem(JOB_DETAILS_COLLAPSED); this.state = { rawLogUrl: '', reftestUrl: '', jobExists: true, jobError: '', revision: null, errors: [], collapseDetails: collapseDetails !== null ? JSON.parse(collapseDetails) : true, highlight: getUrlLineNumber(), repoName: queryString.get('repo'), jobId: queryString.get('job_id'), jobUrl: null, currentRepo: null, }; } async componentDidMount() { const { repoName, jobId } = this.state; const repoPromise = RepositoryModel.getList(); const jobPromise = JobModel.get(repoName, jobId); Promise.all([repoPromise, jobPromise]) .then(async ([repos, job]) => { const currentRepo = repos.find((repo) => repo.name === repoName); // set the title of the browser window/tab document.title = job.searchStr; // This can be later changed to live_backing_log once all of the old logs // called builds-4h are removed const log = job.logs && job.logs.length ? job.logs.find((log) => log.name !== 'errorsummary_json') : null; const rawLogUrl = log.url; // other properties, in order of appearance // Test to disable successful steps checkbox on taskcluster jobs // Test to expose the reftest button in the logviewer actionbar const reftestUrl = rawLogUrl && job.job_group_name && isReftest(job) ? getReftestUrl(rawLogUrl) : null; this.setState( { job, rawLogUrl, reftestUrl, jobExists: true, currentRepo, }, async () => { const params = { taskId: job.task_id, run: job.retry_id, rootUrl: currentRepo.tc_root_url, }; const jobArtifactsPromise = getData(getArtifactsUrl(params)); let builtFromArtifactPromise; if ( currentRepo.name === 'comm-central' || currentRepo.name === 'try-comm-central' ) { builtFromArtifactPromise = getData( getArtifactsUrl({ ...params, ...{ artifactPath: 'public/build/built_from.json' }, }), ); } const pushPromise = PushModel.get(job.push_id); Promise.all([ jobArtifactsPromise, pushPromise, builtFromArtifactPromise, ]).then( async ([artifactsResp, pushResp, builtFromArtifactResp]) => { const { revision } = await pushResp.json(); let jobDetails = !artifactsResp.failureStatus && artifactsResp.data.artifacts ? formatArtifacts(artifactsResp.data.artifacts, params) : []; if ( builtFromArtifactResp && !builtFromArtifactResp.failureStatus ) { jobDetails = [...jobDetails, ...builtFromArtifactResp.data]; } this.setState({ revision, jobUrl: getJobsUrl({ repo: repoName, revision, selectedJob: jobId, }), jobDetails, }); }, ); }, ); }) .catch((error) => { this.setState({ jobExists: false, jobError: error.toString(), }); }); const { data, failureStatus } = await getData( getProjectJobUrl(textLogErrorsEndpoint, jobId), ); if (!failureStatus && data.length) { const errors = data.map((error) => ({ line: error.line, lineNumber: error.line_number + 1, })); const firstErrorLineNumber = errors.length ? [errors[0].lineNumber] : null; const urlLN = getUrlLineNumber(); const highlight = urlLN || firstErrorLineNumber; errorLinesCss(errors); this.setState({ errors }); this.setSelectedLine(highlight, true); } } onHighlight = (range) => { const { highlight } = this.state; const { _start, _end, size } = range; // We can't use null to represent "no highlight", due to: // https://github.com/mozilla-frontend-infra/react-lazylog/issues/22 let newHighlight = -1; if (size === 1) { newHighlight = [_start]; } else if (size > 1) { newHighlight = [_start, _end - 1]; } if (!isEqual(newHighlight, highlight)) { this.setSelectedLine(newHighlight); } }; copySelectedLogToBugFiler = () => { let selectedLogText; if ( document .querySelector('.log-contents') .contains(window.getSelection().anchorNode) && window.getSelection().toString().trim() ) { // Use selection selectedLogText = window.getSelection().toString().trim(); } const descriptionField = window.opener.document.getElementById( 'summary-input', ); const startPos = descriptionField.selectionStart; const endPos = descriptionField.selectionEnd; descriptionField.value = descriptionField.value.substring(0, startPos) + selectedLogText + descriptionField.value.substring(endPos, descriptionField.value.length); descriptionField.selectionStart = startPos + selectedLogText.length; descriptionField.selectionEnd = startPos + selectedLogText.length; const event = document.createEvent('HTMLEvents'); event.initEvent('change', true, true); descriptionField.dispatchEvent(event); }; collapseJobDetails = () => { const { collapseDetails } = this.state; this.setState( { collapseDetails: !collapseDetails, }, () => { localStorage.setItem(JOB_DETAILS_COLLAPSED, !collapseDetails); }, ); }; setSelectedLine = (highlight, scrollToTop) => { this.setState({ highlight }, () => { this.updateQuery({ highlight }); if (highlight && scrollToTop) { this.scrollHighlightToTop(highlight); } }); }; scrollHighlightToTop = (highlight) => { const lineAtTop = highlight && highlight[0] > 7 ? highlight[0] - 7 : 0; scrollToLine(`a[id="${lineAtTop}"]`, 100); }; updateQuery = () => { const { highlight } = this.state; if (highlight < 1) { setUrlParam('lineNumber', null); } else if (highlight.length > 1) { setUrlParam('lineNumber', `${highlight[0]}-${highlight[1]}`); } else { setUrlParam('lineNumber', highlight[0]); } }; // This script has been borrowed from https://github.com/gregtatum/scripts/blob/master/mochitest-formatter/from-talos-log.js logFormatter = (line) => { let color = null; if ( /INFO - GECKO\(\d+\)/.test(line) || /INFO - TEST-UNEXPECTED-FAIL/.test(line) ) { // Do nothing } else if (/INFO - TEST-(OK)|(PASS)/.test(line)) color = '#3B7A3B'; else if (/INFO - TEST-START/.test(line)) color = 'blue'; else if (/INFO - TEST-/.test(line)) color = '#7A7A24'; else if (/!!!/.test(line)) color = '#985E98'; else if (/Browser Chrome Test Summary$/.test(line)) color = '#55677A'; else if (/((INFO -)|([\s]+))(Passed|Failed|Todo):/.test(line)) color = '#55677A'; else if (/INFO/.test(line)) color = '#566262'; return <span style={{ color }}>{line}</span>; }; render() { const { job, rawLogUrl, reftestUrl, jobDetails, jobError, jobExists, revision, errors, collapseDetails, highlight, jobUrl, currentRepo, } = this.state; const extraFields = [ { title: 'Revision', url: jobUrl, value: revision, clipboard: { description: 'full hash', text: revision, }, }, ]; return ( <div className="d-flex flex-column body-logviewer h-100"> <Navigation jobExists={jobExists} result={job ? job.result : ''} jobError={jobError} rawLogUrl={rawLogUrl} reftestUrl={reftestUrl} jobUrl={jobUrl} collapseDetails={collapseDetails} collapseJobDetails={this.collapseJobDetails} copySelectedLogToBugFiler={this.copySelectedLogToBugFiler} /> {job && ( <div className="d-flex flex-column flex-fill"> <Collapse isOpen={!collapseDetails}> <div className="run-data d-flex flex-row mx-1 mb-2"> <div className="d-flex flex-column job-data-panel"> <JobInfo job={job} extraFields={extraFields} revision={revision} className="list-unstyled" showJobFilters={false} currentRepo={currentRepo} /> <JobArtifacts jobDetails={jobDetails} /> </div> <ErrorLines errors={errors} onClickLine={this.setSelectedLine} /> </div> </Collapse> <div className="log-contents flex-fill"> <LazyLog url={rawLogUrl} formatPart={this.logFormatter} scrollToLine={highlight ? highlight[0] : 0} highlight={highlight} selectableLines onHighlight={this.onHighlight} onLoad={() => this.scrollHighlightToTop(highlight)} highlightLineClassName="yellow-highlight" rowHeight={13} extraLines={3} enableSearch caseInsensitive /> </div> </div> )} </div> ); } } export default hot(App);