зеркало из https://github.com/mozilla/treeherder.git
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:
Родитель
2635a241b0
Коммит
50034a713b
|
@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
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 })]);
|
Загрузка…
Ссылка в новой задаче