ui/intermittent-failures/BugDetailsView.jsx (378 lines of code) (raw):
import React from 'react';
import { Row, Col, Breadcrumb, BreadcrumbItem } from 'reactstrap';
import { Link } from 'react-router-dom';
import ReactTable from 'react-table-6';
import PropTypes from 'prop-types';
import { Helmet } from 'react-helmet';
import {
bugDetailsEndpoint,
getJobsUrl,
getLogViewerUrl,
} from '../helpers/url';
import SimpleTooltip from '../shared/SimpleTooltip';
import {
calculateMetrics,
prettyDate,
tableRowStyling,
removePath,
} from './helpers';
import Layout from './Layout';
import withView from './View';
import DateOptions from './DateOptions';
const BugDetailsView = (props) => {
const {
graphData,
tableData,
initialParamsSet,
startday,
endday,
failurehash,
updateState,
updateHash,
bug,
summary,
errorMessages,
lastLocation,
tableFailureStatus,
graphFailureStatus,
uniqueLines,
uniqueFrequency,
} = props;
const customFilter = ({ filter }) => {
if (!tableData || !uniqueLines) return;
return (
<select
onChange={(event) => updateHash(event.target.value)}
style={{ width: '100%' }}
value={filter ? filter.value : failurehash}
>
<option value="all">All</option>
{uniqueLines.map((vals) => {
return (
<option key={vals[1]} value={vals[1]}>
{vals[0]}
</option>
);
})}
</select>
);
};
const lineTrimmer = (failureLines) => {
if (failureLines === undefined) {
return '';
}
if (typeof failureLines === 'string') {
failureLines = failureLines.split('\n');
}
const lines = failureLines.map((i) => i.split('\n'));
const trimmedLines = lines.map((line) => {
const parts = line.toString().split(' | ');
if (parts.length > 2) {
parts.shift();
}
return parts.join(' | ');
});
return trimmedLines.join('\n');
};
const columns = [
{
Header: 'Push Time',
accessor: 'push_time',
minWidth: 105,
className: 'text-left',
},
{
Header: 'Tree',
accessor: 'tree',
},
{
Header: 'Revision',
accessor: 'revision',
Cell: (_props) => (
<a
href={getJobsUrl({
repo: _props.original.tree,
revision: _props.value,
selectedJob: _props.original.job_id,
})}
target="_blank"
rel="noopener noreferrer"
>
{_props.value}
</a>
),
},
{
Header: 'Platform',
accessor: 'platform',
className: 'text-left',
headerClassName: 'platform-column-header',
},
{
Header: 'Build Type',
accessor: 'build_type',
},
{
Header: 'Test Suite',
accessor: 'test_suite',
minWidth: 150,
className: 'text-left',
headerClassName: 'test-suite-header',
},
{
Header: 'Machine Name',
accessor: 'machine_name',
minWidth: 125,
},
{
Header: 'Log',
accessor: 'job_id',
Filter: ({ filter, onChange }) => customFilter({ filter, onChange }),
Cell: (_props) => {
const { value, original } = _props;
return (
<SimpleTooltip
text={
<React.Fragment>
{`${original.lines.length} unexpected-fail${
original.lines.length > 1 ? 's' : ''
}`}
<br />
<a
className="small-text"
href={`${window.location.origin}${getLogViewerUrl(
value,
original.tree,
)}`}
target="_blank"
rel="noopener noreferrer"
>
view details
</a>
</React.Fragment>
}
placement="top"
tooltipText={
original.lines.length && (
<ul>
{original.lines.map((line, index) => (
<li
key={index} // eslint-disable-line react/no-array-index-key
className="failure_li text-truncate"
>
{removePath(line)}
</li>
))}
</ul>
)
}
innerClassName="custom-tooltip"
/>
);
},
minWidth: 110,
},
];
let gOneData = {};
let graphOneData = null;
let graphTwoData = null;
let _tableData = null;
if (graphData.length > 0) {
({ graphOneData, graphTwoData } = calculateMetrics(graphData));
if (uniqueFrequency) {
gOneData = uniqueFrequency;
}
gOneData.all = graphOneData;
gOneData.all[0].count = tableData.length;
_tableData = tableData;
// here we Filter() the tableData.
// Since we use urlParams for failurehash, this synchronizes it.
if (failurehash !== 'all') {
const tData = [];
tableData.forEach((row) => {
const trimmed = lineTrimmer(row.lines);
let filterValue = '';
const hashIndex = 1;
uniqueLines.forEach((uniqueLine) => {
if (trimmed === uniqueLine[0]) {
filterValue = uniqueLine[hashIndex];
}
});
if (filterValue === failurehash) {
tData.push(row);
}
});
_tableData = tData;
}
}
return (
<Layout
{...props}
graphOneData={gOneData}
graphTwoData={graphTwoData}
header={
<React.Fragment>
<Row>
<Helmet>
<title>{`Bug ${bug}${summary ? ` - ${summary}` : ''}`}</title>
</Helmet>
<Col xs="12" className="text-left">
<Breadcrumb listClassName="bg-white">
<BreadcrumbItem>
<a title="Treeherder home page" href="/">
Treeherder
</a>
</BreadcrumbItem>
<BreadcrumbItem>
<Link
title="Intermittent Failures View main page"
to={lastLocation || '/intermittent-failures/'}
>
Main view
</Link>
</BreadcrumbItem>
<BreadcrumbItem active title="Bugdetails view">
Bugdetails view
</BreadcrumbItem>
</Breadcrumb>
</Col>
</Row>
{!errorMessages.length && !tableFailureStatus && !graphFailureStatus && (
<React.Fragment>
<Row>
<Col xs="12" className="mx-auto">
<h1>
<span>Details for Bug </span>
{bug && (
<a
href={`https://bugzilla.mozilla.org/show_bug.cgi?id=${bug}`}
>
{bug}
</a>
)}
</h1>
</Col>
</Row>
<Row>
<Col xs="12" className="mx-auto">
<p className="subheader">{`${prettyDate(
startday,
)} to ${prettyDate(endday)} UTC`}</p>
</Col>
</Row>
{summary && (
<Row>
<Col xs="4" className="mx-auto">
<p className="text-secondary text-center">{summary}</p>
</Col>
</Row>
)}
{tableData.length > 0 && (
<Row>
<Col xs="12" className="mx-auto">
<p className="text-secondary">
{failurehash in gOneData
? gOneData[failurehash][0].count
: 0}{' '}
total failures
</p>
</Col>
</Row>
)}
</React.Fragment>
)}
</React.Fragment>
}
table={
bug &&
initialParamsSet &&
_tableData && (
<ReactTable
data={_tableData}
filterable
showPageSizeOptions
columns={columns}
className="-striped"
getTrProps={tableRowStyling}
showPaginationTop
defaultPageSize={50}
/>
)
}
datePicker={<DateOptions updateState={updateState} />}
/>
);
};
BugDetailsView.propTypes = {
location: PropTypes.shape({
pathname: PropTypes.string,
search: PropTypes.string,
state: PropTypes.shape({}),
hash: PropTypes.string,
}).isRequired,
lastLocation: PropTypes.shape({
pathname: PropTypes.string,
search: PropTypes.string,
state: PropTypes.shape({}),
hash: PropTypes.string,
}).isRequired,
tree: PropTypes.string.isRequired,
updateAppState: PropTypes.func,
updateState: PropTypes.func.isRequired,
updateHash: PropTypes.func.isRequired,
startday: PropTypes.string.isRequired,
failurehash: PropTypes.string.isRequired,
endday: PropTypes.string.isRequired,
tableData: PropTypes.arrayOf(
PropTypes.shape({
// Define the expected structure of tableData objects here
push_time: PropTypes.string,
tree: PropTypes.string,
revision: PropTypes.string,
platform: PropTypes.string,
build_type: PropTypes.string,
test_suite: PropTypes.string,
machine_name: PropTypes.string,
job_id: PropTypes.string,
lines: PropTypes.arrayOf(PropTypes.string),
}),
),
graphData: PropTypes.arrayOf(
PropTypes.shape({
// Define the expected structure of graphData objects here
// Example:
timestamp: PropTypes.number,
value: PropTypes.number,
}),
),
initialParamsSet: PropTypes.bool.isRequired,
bug: PropTypes.string.isRequired,
summary: PropTypes.string.isRequired,
errorMessages: PropTypes.arrayOf(PropTypes.string),
tableFailureStatus: PropTypes.string,
graphFailureStatus: PropTypes.string,
uniqueLines: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)),
uniqueFrequency: PropTypes.shape({
// Define the expected structure of uniqueFrequency object here
all: PropTypes.arrayOf(
PropTypes.shape({
count: PropTypes.number,
}),
),
}),
};
BugDetailsView.defaultProps = {
graphData: [],
tableData: [],
errorMessages: [],
tableFailureStatus: null,
graphFailureStatus: null,
updateAppState: null,
uniqueLines: [],
uniqueFrequency: {},
};
const defaultState = {
endpoint: bugDetailsEndpoint,
route: '/bugdetails',
};
export default withView(defaultState)(BugDetailsView);