Bug 1444207 - Consolidate failure_summary_panel to /details-panel (#3341)

* Rename the component to a *Tab and move to /details-panel folder
* cleanup indentation
* cleanup props and panel elements
* Use deconstruction for props object
* simplify onclick event calls with anonymous functions
* Move filerInAddress logic to FailureSummaryTab
* Move the data-fetching into the main controller like the other
  tabs so we can do away with the special controller for the
  failure summary tab.
* Move functions to helpers instead of filters and take less
  values as params
* Eliminate failure_summary/controller
* Moved logic to either the parent controller or into helpers and
  the FailureSummaryTab
* Use helper function for bugzilla url
This commit is contained in:
Cameron Dawson 2018-03-16 08:55:04 -07:00 коммит произвёл GitHub
Родитель 2635a241b0
Коммит 50034a713b
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
8 изменённых файлов: 377 добавлений и 429 удалений

Просмотреть файл

@ -415,6 +415,7 @@ ul.failure-summary-list li .btn-xs {
.show-hide-more {
padding: 0 0 0 37px;
color: #0000ee;
cursor: pointer;
}
/*

Просмотреть файл

@ -0,0 +1,240 @@
import PropTypes from 'prop-types';
import treeherder from '../js/treeherder';
import { getBugUrl } from '../helpers/urlHelper';
import { escapeHTML, highlightCommonTerms } from "../helpers/displayHelper";
const BUG_LIMIT = 20;
class SuggestionsListItem extends React.Component {
constructor(props) {
super(props);
this.state = {
suggestionShowMore: false,
};
this.clickShowMore = this.clickShowMore.bind(this);
}
clickShowMore() {
this.setState({ suggestionShowMore: !this.state.suggestionShowMore });
}
render() {
const {
suggestion, selectedJob, $timeout, pinboardService, fileBug, index
} = this.props;
const { suggestionShowMore } = this.state;
return (
<li>
<div className="job-tabs-content">
<span
className="btn btn-xs btn-light-bordered link-style"
onClick={() => fileBug(index)}
title="file a bug for this failure"
>
<i className="fa fa-bug" />
</span>
<span>{suggestion.search}</span>
</div>
{/* <!--Open recent bugs--> */}
{suggestion.valid_open_recent &&
<ul className="list-unstyled failure-summary-bugs">
{suggestion.bugs.open_recent.map(bug =>
(<BugListItem
key={bug.id}
bug={bug}
selectedJob={selectedJob}
pinboardService={pinboardService}
suggestion={suggestion}
$timeout={$timeout}
/>))}
</ul>}
{/* <!--All other bugs--> */}
{suggestion.valid_all_others && suggestion.valid_open_recent &&
<span
rel="noopener"
onClick={this.clickShowMore}
className="show-hide-more"
>Show / Hide more</span>}
{suggestion.valid_all_others && (suggestionShowMore
|| !suggestion.valid_open_recent) &&
<ul className="list-unstyled failure-summary-bugs">
{suggestion.bugs.all_others.map(bug =>
(<BugListItem
key={bug.id}
bug={bug}
selectedJob={selectedJob}
pinboardService={pinboardService}
suggestion={suggestion}
$timeout={$timeout}
bugClassName={bug.resolution !== "" ? "deleted" : ""}
title={bug.resolution !== "" ? bug.resolution : ""}
/>))}
</ul>}
{(suggestion.bugs.too_many_open_recent || (suggestion.bugs.too_many_all_others
&& !suggestion.valid_open_recent)) &&
<mark>Exceeded max {BUG_LIMIT} bug suggestions, most of which are likely false positives.</mark>}
</li>
);
}
}
function ListItem(props) {
return (
<li>
<p className="failure-summary-line-empty mb-0">{props.text}</p>
</li>
);
}
function BugListItem(props) {
const {
bug, suggestion,
bugClassName, title, $timeout, pinboardService, selectedJob,
} = props;
const bugUrl = getBugUrl(bug.id);
const bugSummaryText = escapeHTML(bug.summary);
const highlightedTerms = { __html: highlightCommonTerms(bugSummaryText, suggestion.search) };
return (
<li>
<button
className="btn btn-xs btn-light-bordered"
onClick={() => $timeout(() => pinboardService.addBug(bug, selectedJob))}
title="add to list of bugs to associate with all pinned jobs"
>
<i className="fa fa-thumb-tack" />
</button>
<a
className={`${bugClassName} ml-1`}
href={bugUrl}
target="_blank"
rel="noopener"
title={title}
>{bug.id}
<span className={`${bugClassName} ml-1`} dangerouslySetInnerHTML={highlightedTerms} />
</a>
</li>
);
}
function ErrorsList(props) {
const errorListItem = props.errors.map((error, key) => (
<li
key={key} // eslint-disable-line react/no-array-index-key
>{error.name} : {error.result}.
<a
title="Open in Log Viewer"
target="_blank"
rel="noopener"
href={error.lvURL}
><span className="ml-1">View log</span></a>
</li>
));
return (
<li>
No Bug Suggestions Available.<br />
<span className="font-weight-bold">Unsuccessful Execution Steps</span>
<ul>{errorListItem}</ul>
</li>
);
}
class FailureSummaryTab extends React.Component {
constructor(props) {
super(props);
const { $injector } = this.props;
this.$timeout = $injector.get('$timeout');
this.thPinboard = $injector.get('thPinboard');
}
render() {
const {
fileBug, jobLogUrls, logParseStatus, suggestions, errors,
bugSuggestionsLoading, selectedJob
} = this.props;
const logs = jobLogUrls;
const jobLogsAllParsed = logs ? logs.every(jlu => (jlu.parse_status !== 'pending')) : false;
return (
<ul className="list-unstyled failure-summary-list" ref={this.fsMount}>
{suggestions && suggestions.map((suggestion, index) =>
(<SuggestionsListItem
key={index} // eslint-disable-line react/no-array-index-key
index={index}
suggestion={suggestion}
fileBug={fileBug}
pinboardService={this.thPinboard}
selectedJob={selectedJob}
$timeout={this.$timeout}
/>))}
{errors && errors.length > 0 &&
<ErrorsList errors={errors} />}
{!bugSuggestionsLoading && jobLogsAllParsed && logs &&
logs.length === 0 && suggestions.length === 0 && errors.length === 0 &&
<ListItem text="Failure summary is empty" />}
{!bugSuggestionsLoading && jobLogsAllParsed && logs && logs.length > 0 &&
logParseStatus === 'success' &&
<li>
<p className="failure-summary-line-empty mb-0">Log parsing complete. Generating bug suggestions.<br />
<span>The content of this panel will refresh in 5 seconds.</span></p>
</li>}
{logs && !bugSuggestionsLoading && !jobLogsAllParsed &&
logs.map(jobLog =>
(<li key={jobLog.id}>
<p className="failure-summary-line-empty mb-0">Log parsing in progress.<br />
<a
title="Open the raw log in a new window"
target="_blank"
rel="noopener"
href={jobLog.url}
>The raw log</a> is available. This panel will automatically recheck every 5 seconds.</p>
</li>))}
{!bugSuggestionsLoading && logParseStatus === 'failed' &&
<ListItem text="Log parsing failed. Unable to generate failure summary." />}
{!bugSuggestionsLoading && logs && logs.length === 0 &&
<ListItem text="No logs available for this job." />}
{bugSuggestionsLoading &&
<div className="overlay">
<div>
<span className="fa fa-spinner fa-pulse th-spinner-lg" />
</div>
</div>}
</ul>
);
}
}
FailureSummaryTab.propTypes = {
suggestions: PropTypes.array,
fileBug: PropTypes.func,
selectedJob: PropTypes.object,
$injector: PropTypes.object,
errors: PropTypes.array,
bugSuggestionsLoading: PropTypes.bool,
jobLogUrls: PropTypes.array,
logParseStatus: PropTypes.string
};
treeherder.directive('failureSummaryTab', ['reactDirective', '$injector', (reactDirective, $injector) =>
reactDirective(FailureSummaryTab, undefined, {}, { $injector })]);

Просмотреть файл

@ -72,10 +72,9 @@ import './js/controllers/tcjobactions';
import './plugins/tabs';
import './plugins/controller';
import './details-panel/JobDetailsPane';
import './plugins/failure_summary_panel';
import './details-panel/FailureSummaryTab';
import './details-panel/AnnotationsTab';
import './plugins/pinboard';
import './plugins/failure_summary/controller';
import './plugins/similar_jobs/controller';
import './plugins/auto_classification/controller';
import './js/filters';

Просмотреть файл

@ -1,5 +1,3 @@
// Remove disabling when there is more than one export in the file.
// eslint-disable-next-line import/prefer-default-export
export const toDateStr = function toDateStr(timestamp) {
const dateFormat = {
weekday: 'short',
@ -12,3 +10,30 @@ export const toDateStr = function toDateStr(timestamp) {
};
return new Date(timestamp * 1000).toLocaleString("en-US", dateFormat);
};
export const escapeHTML = function escapeHTML(text) {
if (text) {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/'/g, '&#39;')
.replace(/"/g, '&quot;');
}
return '';
};
export const highlightCommonTerms = function highlightCommonTerms(input, compareStr) {
const tokens = compareStr.split(/[^a-zA-Z0-9_-]+/);
tokens.sort((a, b) => (b.length - a.length));
tokens.forEach((elem) => {
if (elem.length > 0) {
input = input.replace(
new RegExp(`(^|\\W)(${elem})($|\\W)`, 'gi'),
(match, prefix, token, suffix) => `${prefix}<strong>${token}</strong>${suffix}`
);
}
});
return input;
};

Просмотреть файл

@ -4,6 +4,7 @@ import { Queue, slugid } from 'taskcluster-client-web';
import treeherder from '../js/treeherder';
import thTaskcluster from '../js/services/taskcluster';
import tcJobActionsTemplate from '../partials/main/tcjobactions.html';
import intermittentTemplate from '../partials/main/intermittent.html';
import { getStatus } from '../helpers/jobHelper';
import { getBugUrl, getSlaveHealthUrl, getInspectTaskUrl, getLogViewerUrl } from '../helpers/urlHelper';
@ -15,7 +16,7 @@ treeherder.controller('PluginCtrl', [
'$q', 'thPinboard',
'ThJobDetailModel', 'thBuildApi', 'thNotify', 'ThJobLogUrlModel', 'ThModelErrors', 'ThTaskclusterErrors',
'thTabs', '$timeout', 'thReftestStatus', 'ThResultSetStore',
'PhSeries', 'tcactions',
'PhSeries', 'tcactions', 'ThBugSuggestionsModel', 'ThTextLogStepModel',
function PluginCtrl(
$scope, $rootScope, $location, $http, $interpolate, $uibModal,
ThJobClassificationModel,
@ -24,7 +25,7 @@ treeherder.controller('PluginCtrl', [
$q, thPinboard,
ThJobDetailModel, thBuildApi, thNotify, ThJobLogUrlModel, ThModelErrors, ThTaskclusterErrors, thTabs,
$timeout, thReftestStatus, ThResultSetStore, PhSeries,
tcactions) {
tcactions, ThBugSuggestionsModel, ThTextLogStepModel) {
$scope.job = {};
$scope.revisionList = [];
@ -76,11 +77,102 @@ treeherder.controller('PluginCtrl', [
}
};
$scope.loadBugSuggestions = function () {
$scope.errors = [];
ThBugSuggestionsModel.query({
project: $rootScope.repoName,
jobId: $scope.job.id
}, (suggestions) => {
suggestions.forEach(function (suggestion) {
suggestion.bugs.too_many_open_recent = (
suggestion.bugs.open_recent.length > $scope.bug_limit
);
suggestion.bugs.too_many_all_others = (
suggestion.bugs.all_others.length > $scope.bug_limit
);
suggestion.valid_open_recent = (
suggestion.bugs.open_recent.length > 0 &&
!suggestion.bugs.too_many_open_recent
);
suggestion.valid_all_others = (
suggestion.bugs.all_others.length > 0 &&
!suggestion.bugs.too_many_all_others &&
// If we have too many open_recent bugs, we're unlikely to have
// relevant all_others bugs, so don't show them either.
!suggestion.bugs.too_many_open_recent
);
});
// if we have no bug suggestions, populate with the raw errors from
// the log (we can do this asynchronously, it should normally be
// fast)
if (!suggestions.length) {
ThTextLogStepModel.query({
project: $rootScope.repoName,
jobId: $scope.job.id
}, function (textLogSteps) {
$scope.errors = textLogSteps
.filter(step => step.result !== 'success')
.map(function (step) {
return {
name: step.name,
result: step.result,
lvURL: getLogViewerUrl($scope.job.id, $rootScope.repoName, step.finished_line_number)
};
});
});
}
$scope.suggestions = suggestions;
$scope.bugSuggestionsLoading = false;
});
};
$scope.fileBug = function (index) {
const summary = $scope.suggestions[index].search;
const crashRegex = /application crashed \[@ (.+)\]$/g;
const crash = summary.match(crashRegex);
const crashSignatures = crash ? [crash[0].split("application crashed ")[1]] : [];
const allFailures = $scope.suggestions.map(sugg => (sugg.search.split(" | ")));
const modalInstance = $uibModal.open({
template: intermittentTemplate,
controller: 'BugFilerCtrl',
size: 'lg',
openedClass: "filer-open",
resolve: {
summary: () => (summary),
search_terms: () => ($scope.suggestions[index].search_terms),
fullLog: () => ($scope.job_log_urls[0].url),
parsedLog: () => ($scope.lvFullUrl),
reftest: () => ($scope.isReftest() ? $scope.reftestUrl : ""),
selectedJob: () => ($scope.selectedJob),
allFailures: () => (allFailures),
crashSignatures: () => (crashSignatures),
successCallback: () => (data) => {
// Auto-classify this failure now that the bug has been filed
// and we have a bug number
thPinboard.addBug({ id: data.success });
$rootScope.$evalAsync(
$rootScope.$emit(
thEvents.saveClassification));
// Open the newly filed bug in a new tab or window for further editing
window.open(getBugUrl(data.success));
}
}
});
thPinboard.pinJob($scope.selectedJob);
modalInstance.opened.then(function () {
window.setTimeout(() => modalInstance.initiate(), 0);
});
};
// this promise will void all the ajax requests
// triggered by selectJob once resolved
let selectJobPromise = null;
const selectJob = function (job) {
$scope.bugSuggestionsLoading = true;
// make super-extra sure that the autoclassify tab shows up when it should
showAutoClassifyTab();
@ -151,11 +243,6 @@ treeherder.controller('PluginCtrl', [
$scope.logParseStatus = $scope.job_log_urls[0].parse_status;
}
// Provide a parse status for the model
$scope.jobLogsAllParsed = _.every($scope.job_log_urls, function (jlu) {
return jlu.parse_status !== 'pending';
});
$scope.lvUrl = getLogViewerUrl($scope.job.id, $scope.repoName);
$scope.lvFullUrl = location.origin + "/" + $scope.lvUrl;
if ($scope.job_log_urls.length) {
@ -185,6 +272,7 @@ treeherder.controller('PluginCtrl', [
$scope.updateClassifications();
$scope.updateBugs();
$scope.loadBugSuggestions();
$scope.job_detail_loading = false;
});

Просмотреть файл

@ -1,161 +0,0 @@
import angular from 'angular';
import treeherder from '../../js/treeherder';
import intermittentTemplate from '../../partials/main/intermittent.html';
import { getLogViewerUrl } from '../../helpers/urlHelper';
treeherder.controller('BugsPluginCtrl', [
'$scope', '$rootScope', 'ThTextLogStepModel',
'ThBugSuggestionsModel', 'thPinboard', 'thEvents',
'thTabs', '$uibModal', '$location',
function BugsPluginCtrl(
$scope, $rootScope, ThTextLogStepModel, ThBugSuggestionsModel,
thPinboard, thEvents, thTabs, $uibModal, $location) {
$scope.bug_limit = 20;
$scope.tabs = thTabs.tabs;
$scope.filerInAddress = false;
let query;
// update function triggered by the plugins controller
thTabs.tabs.failureSummary.update = function () {
const newValue = thTabs.tabs.failureSummary.contentId;
$scope.suggestions = [];
$scope.bugSuggestionsLoaded = false;
// cancel any existing failure summary queries
if (query) {
query.$cancelRequest();
}
if (angular.isDefined(newValue)) {
thTabs.tabs.failureSummary.is_loading = true;
query = ThBugSuggestionsModel.query({
project: $rootScope.repoName,
jobId: newValue
}, function (suggestions) {
suggestions.forEach(function (suggestion) {
suggestion.bugs.too_many_open_recent = (
suggestion.bugs.open_recent.length > $scope.bug_limit
);
suggestion.bugs.too_many_all_others = (
suggestion.bugs.all_others.length > $scope.bug_limit
);
suggestion.valid_open_recent = (
suggestion.bugs.open_recent.length > 0 &&
!suggestion.bugs.too_many_open_recent
);
suggestion.valid_all_others = (
suggestion.bugs.all_others.length > 0 &&
!suggestion.bugs.too_many_all_others &&
// If we have too many open_recent bugs, we're unlikely to have
// relevant all_others bugs, so don't show them either.
!suggestion.bugs.too_many_open_recent
);
});
// if we have no bug suggestions, populate with the raw errors from
// the log (we can do this asynchronously, it should normally be
// fast)
if (!suggestions.length) {
query = ThTextLogStepModel.query({
project: $rootScope.repoName,
jobId: newValue
}, function (textLogSteps) {
$scope.errors = textLogSteps
.filter(step => step.result !== 'success')
.map(function (step) {
return {
name: step.name,
result: step.result,
lvURL: getLogViewerUrl(newValue, $rootScope.repoName, step.finished_line_number)
};
});
});
}
$scope.suggestions = suggestions;
$scope.bugSuggestionsLoaded = true;
thTabs.tabs.failureSummary.is_loading = false;
});
}
};
const showBugFilerButton = function () {
$scope.filerInAddress = $location.search().bugfiler === true;
};
showBugFilerButton();
$rootScope.$on('$locationChangeSuccess', function () {
showBugFilerButton();
});
$scope.fileBug = function (index) {
const summary = $scope.suggestions[index].search;
const allFailures = [];
const crashSignatures = [];
const crashRegex = /application crashed \[@ (.+)\]$/g;
const crash = summary.match(crashRegex);
if (crash) {
const signature = crash[0].split("application crashed ")[1];
crashSignatures.push(signature);
}
for (let i=0; i<$scope.suggestions.length; i++) {
allFailures.push($scope.suggestions[i].search.split(" | "));
}
const modalInstance = $uibModal.open({
template: intermittentTemplate,
controller: 'BugFilerCtrl',
size: 'lg',
openedClass: "filer-open",
resolve: {
summary: function () {
return summary;
},
search_terms: function () {
return $scope.suggestions[index].search_terms;
},
fullLog: function () {
return $scope.job_log_urls[0].url;
},
parsedLog: function () {
return $scope.lvFullUrl;
},
reftest: function () {
return $scope.isReftest() ? $scope.reftestUrl : "";
},
selectedJob: function () {
return $scope.selectedJob;
},
allFailures: function () {
return allFailures;
},
crashSignatures: function () {
return crashSignatures;
},
successCallback: function () {
return function (data) {
// Auto-classify this failure now that the bug has been filed
// and we have a bug number
thPinboard.addBug({ id: data.success });
$rootScope.$evalAsync(
$rootScope.$emit(
thEvents.saveClassification));
// Open the newly filed bug in a new tab or window for further editing
window.open("https://bugzilla.mozilla.org/show_bug.cgi?id=" + data.success);
};
}
}
});
thPinboard.pinJob($scope.selectedJob);
modalInstance.opened.then(function () {
window.setTimeout(() => modalInstance.initiate(), 0);
});
};
}
]);

Просмотреть файл

@ -1,11 +1,14 @@
<div ng-controller="BugsPluginCtrl">
<div class="w-100 h-100">
<failure-summary-panel
tabs="tabs" suggestions="suggestions" filerInAddress="filerInAddress"
fileBug="fileBug" user="user" bugLimit="bug_limit"
pinboardService="pinboard_service" selectedJob="selectedJob" errors="errors"
bugSuggestionsLoaded="bugSuggestionsLoaded" jobLogsAllParsed="jobLogsAllParsed"
jobLogUrls="job_log_urls" logParseStatus="logParseStatus">
</failure-summary-panel>
</div>
<div>
<div class="w-100 h-100">
<failure-summary-tab
suggestions="suggestions"
fileBug="fileBug"
selectedJob="selectedJob"
errors="errors"
bugSuggestionsLoading="bugSuggestionsLoading"
logs="job_log_urls"
jobLogUrls="job_log_urls"
logParseStatus="logParseStatus"
/>
</div>
</div>

Просмотреть файл

@ -1,247 +0,0 @@
import PropTypes from 'prop-types';
import treeherder from '../js/treeherder';
import { getBugUrl } from '../helpers/urlHelper';
class SuggestionsListItem extends React.Component {
constructor(props) {
super(props);
this.state = {
suggestionShowMore: false
};
this.fileBugEvent = this.fileBugEvent.bind(this);
}
fileBugEvent(event) {
event.preventDefault();
this.props.fileBug(this.props.index);
}
clickShowMore(event) {
event.preventDefault();
this.setState({ suggestionShowMore: !this.state.suggestionShowMore });
}
render() {
//If this method is bound in the constructor it gives me a warning about only setting state in a mounted component
// but if we move to allowing the arrow function in classes at some point, this problem should be solved.
this.clickShowMore = this.clickShowMore.bind(this);
return (
<li>
<div className="job-tabs-content">
{(this.props.filerInAddress || this.props.user.is_staff) &&
<a
className="btn btn-xs btn-light-bordered"
onClick={this.fileBugEvent}
title="file a bug for this failure"
>
<i className="fa fa-bug" />
</a>}
<span>{this.props.suggestion.search}</span>
</div>
{/* <!--Open recent bugs--> */}
{this.props.suggestion.valid_open_recent &&
<ul className="list-unstyled failure-summary-bugs">
{this.props.suggestion.bugs.open_recent.map(bug =>
(<BugListItem
key={bug.id}
bug={bug}
selectedJob={this.props.selectedJob}
pinboardService={this.props.pinboardService}
escapeHTMLFilter={this.props.escapeHTMLFilter}
suggestion={this.props.suggestion}
highlightCommonTermsFilter={this.props.highlightCommonTermsFilter}
$timeout={this.props.$timeout}
/>))}
</ul>}
{/* <!--All other bugs--> */}
{this.props.suggestion.valid_all_others && this.props.suggestion.valid_open_recent &&
<a
target="_blank"
rel="noopener"
href=""
onClick={this.clickShowMore}
className="show-hide-more"
>Show / Hide more</a>}
{this.props.suggestion.valid_all_others && (this.state.suggestionShowMore
|| !this.props.suggestion.valid_open_recent) &&
<ul className="list-unstyled failure-summary-bugs">
{this.props.suggestion.bugs.all_others.map(bug =>
(<BugListItem
key={bug.id}
bug={bug}
selectedJob={this.props.selectedJob}
pinboardService={this.props.pinboardService}
escapeHTMLFilter={this.props.escapeHTMLFilter}
suggestion={this.props.suggestion}
highlightCommonTermsFilter={this.props.highlightCommonTermsFilter}
$timeout={this.props.$timeout}
bugClassName={bug.resolution !== "" ? "deleted" : ""}
title={bug.resolution !== "" ? bug.resolution : ""}
/>))}
</ul>}
{(this.props.suggestion.bugs.too_many_open_recent || (this.props.suggestion.bugs.too_many_all_others
&& !this.props.suggestion.valid_open_recent)) &&
<mark>Exceeded max {this.props.bugLimit} bug suggestions, most of which are likely false positives.</mark>}
</li>
);
}
}
function ListItem(props) {
return (
<li>
<p className="failure-summary-line-empty mb-0">{props.text}</p>
</li>
);
}
function BugListItem(props) {
const pinboardServiceEvent = () => {
const { bug, selectedJob, pinboardService, $timeout } = props;
$timeout(() => (pinboardService.addBug(bug, selectedJob)));
};
const bugUrl = getBugUrl(props.bug.id);
const bugSummaryText = props.escapeHTMLFilter(props.bug.summary);
const bugSummaryHTML = { __html: props.highlightCommonTermsFilter(bugSummaryText, props.suggestion.search) };
return (
<li>
<button
className="btn btn-xs btn-light-bordered"
onClick={pinboardServiceEvent}
title="add to list of bugs to associate with all pinned jobs"
>
<i className="fa fa-thumb-tack" />
</button>
<a
className={`${props.bugClassName} ml-1`}
href={bugUrl}
target="_blank"
rel="noopener"
title={props.title}
>{props.bug.id}
<span className={`${props.bugClassName} ml-1`} dangerouslySetInnerHTML={bugSummaryHTML} />
</a>
</li>
);
}
function ErrorsList(props) {
const errorListItem = props.errors.map((error, key) => (
<li
key={key} // eslint-disable-line react/no-array-index-key
>{error.name} : {error.result}.
<a
title="Open in Log Viewer"
target="_blank"
rel="noopener"
href={error.lvURL}
><span className="ml-1">View log</span></a>
</li>
));
return (
<li>
No Bug Suggestions Available.<br />
<span className="font-weight-bold">Unsuccessful Execution Steps</span>
<ul>{errorListItem}</ul>
</li>
);
}
function FailureSummaryPanel(props) {
const escapeHTMLFilter = props.$injector.get('$filter')('escapeHTML');
const highlightCommonTermsFilter = props.$injector.get('$filter')('highlightCommonTerms');
const $timeout = props.$injector.get('$timeout');
return (
<ul className="list-unstyled failure-summary-list">
{props.suggestions && props.suggestions.map((suggestion, index) =>
(<SuggestionsListItem
key={index} // eslint-disable-line react/no-array-index-key
index={index}
suggestion={suggestion}
user={props.user}
filerInAddress={props.filerInAddress}
fileBug={props.fileBug}
highlightCommonTermsFilter={highlightCommonTermsFilter}
escapeHTMLFilter={escapeHTMLFilter}
bugLimit={props.bugLimit}
pinboardService={props.pinboardService}
selectedJob={props.selectedJob}
$timeout={$timeout}
/>))}
{props.errors && props.errors.length > 0 &&
<ErrorsList errors={props.errors} />}
{!props.tabs.failureSummary.is_loading && props.jobLogsAllParsed && props.bugSuggestionsLoaded &&
props.jobLogUrls.length === 0 && props.suggestions.length === 0 && props.errors.length === 0 &&
<ListItem text="Failure summary is empty" />}
{!props.tabs.failureSummary.is_loading && props.jobLogsAllParsed && !props.bugSuggestionsLoaded
&& props.jobLogUrls.length && props.logParseStatus === 'success' &&
<li>
<p className="failure-summary-line-empty mb-0">Log parsing complete. Generating bug suggestions.<br />
<span>The content of this panel will refresh in 5 seconds.</span></p>
</li>}
{props.jobLogUrls && !props.tabs.failureSummary.is_loading && !props.jobLogsAllParsed &&
props.jobLogUrls.map(jobLog =>
(<li key={jobLog.id}>
<p className="failure-summary-line-empty mb-0">Log parsing in progress.<br />
<a
title="Open the raw log in a new window"
target="_blank"
rel="noopener"
href={jobLog.url}
>The raw log</a> is available. This panel will automatically recheck every 5 seconds.</p>
</li>))}
{!props.tabs.failureSummary.is_loading && props.logParseStatus === 'failed' &&
<ListItem text="Log parsing failed. Unable to generate failure summary." />}
{!props.tabs.failureSummary.is_loading && props.jobLogUrls && props.jobLogUrls.length === 0 &&
<ListItem text="No logs available for this job." />}
{props.tabs.failureSummary.is_loading &&
<div className="overlay">
<div>
<span className="fa fa-spinner fa-pulse th-spinner-lg" />
</div>
</div>}
</ul>
);
}
FailureSummaryPanel.propTypes = {
tabs: PropTypes.object,
suggestions: PropTypes.array,
filerInAddress: PropTypes.bool,
fileBug: PropTypes.func,
user: PropTypes.object,
pinboardService: PropTypes.object,
selectedJob: PropTypes.object,
$injector: PropTypes.object,
bugLimit: PropTypes.number,
errors: PropTypes.array,
bugSuggestionsLoaded: PropTypes.bool,
jobLogsAllParsed: PropTypes.bool,
jobLogUrls: PropTypes.array,
logParseStatus: PropTypes.string
};
treeherder.directive('failureSummaryPanel', ['reactDirective', '$injector', (reactDirective, $injector) =>
reactDirective(FailureSummaryPanel, undefined, {}, { $injector })]);