Bug 1450018 - Convert Similar jobs tab to ReactJS (#3455)

This commit is contained in:
Cameron Dawson 2018-04-25 13:29:51 -07:00 коммит произвёл GitHub
Родитель 47ac771a84
Коммит e09775179b
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
9 изменённых файлов: 331 добавлений и 283 удалений

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

@ -0,0 +1,303 @@
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from "react2angular/index";
import { getBtnClass, getStatus } from '../helpers/jobHelper';
import { toDateStr, toShortDateStr } from '../helpers/displayHelper';
import { getSlaveHealthUrl, getJobsUrl } from '../helpers/urlHelper';
import treeherder from "../js/treeherder";
import { thEvents } from "../js/constants";
class SimilarJobsTab extends React.Component {
constructor(props) {
super(props);
const { $injector } = this.props;
this.ThJobModel = $injector.get('ThJobModel');
this.$rootScope = $injector.get('$rootScope');
this.ThTextLogStepModel = $injector.get('ThTextLogStepModel');
this.ThResultSetModel = $injector.get('ThResultSetModel');
this.thNotify = $injector.get('thNotify');
this.thTabs = $injector.get('thTabs');
this.thClassificationTypes = $injector.get('thClassificationTypes');
this.pageSize = 20;
this.tab = this.thTabs.tabs.similarJobs;
this.state = {
similarJobs: [],
filterBuildPlatformId: true,
filterOptionCollectionHash: true,
page: 1,
selectedSimilarJob: null,
hasNextPage: false,
selectedJob: null,
isLoading: true,
};
// map between state fields and job fields
this.filterMap = {
filterBuildPlatformId: 'build_platform_id',
filterOptionCollectionHash: 'option_collection_hash',
};
}
componentDidMount() {
this.getSimilarJobs = this.getSimilarJobs.bind(this);
this.showNext = this.showNext.bind(this);
this.toggleFilter = this.toggleFilter.bind(this);
this.jobClickUnlisten = this.$rootScope.$on(thEvents.jobClick, (event, job) => {
this.setState({ selectedJob: job, similarJobs: [], isLoading: true }, this.getSimilarJobs);
});
}
componentWillUnmount() {
this.jobClickUnlisten();
}
async getSimilarJobs() {
const { page, selectedJob, similarJobs, selectedSimilarJob } = this.state;
const { repoName } = 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
};
['filterBuildPlatformId', 'filterOptionCollectionHash']
.forEach((key) => {
if (this.state[key]) {
const field = this.filterMap[key];
options[field] = selectedJob[field];
}
});
const newSimilarJobs = await this.ThJobModel.get_similar_jobs(repoName, selectedJob.id, options);
if (newSimilarJobs.length > 0) {
this.setState({ hasNextPage: newSimilarJobs.length > this.pageSize });
newSimilarJobs.pop();
// create an array of unique push ids
const pushIds = [...new Set(newSimilarJobs.map(job => job.result_set_id))];
// get pushes and revisions for the given ids
const pushListResp = await this.ThResultSetModel.getResultSetList(repoName, pushIds, true);
const pushList = pushListResp.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.result_set_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]);
}
}
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 } = this.props;
this.ThJobModel.get(repoName, job.id)
.then((nextJob) => {
nextJob.result_status = getStatus(nextJob);
nextJob.duration = nextJob.end_timestamp - nextJob.start_timestamp / 60;
nextJob.failure_classification = this.thClassificationTypes.classifications[
nextJob.failure_classification_id];
//retrieve the list of error lines
this.ThTextLogStepModel.query({
project: repoName,
jobId: nextJob.id
}, (textLogSteps) => {
nextJob.error_lines = textLogSteps.reduce((acc, step) => (
[...acc, ...step.errors]), []);
this.setState({ selectedSimilarJob: nextJob });
});
});
}
toggleFilter(filterField) {
this.setState(
{ [filterField]: !this.state[filterField], similarJobs: [], isLoading: true },
this.getSimilarJobs
);
}
render() {
const {
similarJobs,
selectedSimilarJob,
hasNextPage,
filterOptionCollectionHash,
filterBuildPlatformId,
isLoading,
} = this.state;
const button_class = job => getBtnClass(getStatus(job));
const selectedSimilarJobId = selectedSimilarJob ? selectedSimilarJob.id : null;
return (
<div className="similar_jobs w-100">
<div className="left_panel">
<table className="table table-super-condensed table-hover">
<thead>
<tr>
<th>Job</th>
<th>Pushed</th>
<th>Author</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 ${button_class(similarJob)}`}
>{similarJob.job_type_symbol}
{similarJob.failure_classification_id > 1 &&
<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>
<a href={similarJob.revisionResultsetFilterUrl}>
{similarJob.result_set.revisions[0].revision}
</a>
</td>
</tr>))}
</tbody>
</table>
{hasNextPage &&
<button
className="btn btn-light-bordered btn-sm link-style"
onClick={this.showNext}
>Show previous jobs</button>}
</div>
<div className="right_panel">
<form className="form form-inline">
<div className="checkbox">
<input
onChange={() => this.toggleFilter('filterBuildPlatformId')}
type="checkbox"
checked={filterBuildPlatformId}
/>
<small>Same platform</small>
</div>
<div className="checkbox">
<input
onChange={() => this.toggleFilter('filterOptionCollectionHash')}
type="checkbox"
checked={filterOptionCollectionHash}
/>
<small>Same options</small>
</div>
</form>
<div className="similar_job_detail">
{selectedSimilarJob && <table className="table table-super-condensed">
<tbody>
<tr>
<th>Result</th>
<td>{selectedSimilarJob.result_status}</td>
</tr>
<tr>
<th>Machine name</th>
<td>
<a
target="_blank"
rel="noopener"
href={getSlaveHealthUrl(selectedSimilarJob.machine_name)}
>{selectedSimilarJob.machine_name}</a>
</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>
<label
className={`badge ${selectedSimilarJob.failure_classification.star}`}
>{selectedSimilarJob.failure_classification.name}</label>
</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>
<span className="fa fa-spinner fa-pulse th-spinner-lg" />
</div>
</div>}
</div>
);
}
}
SimilarJobsTab.propTypes = {
$injector: PropTypes.object.isRequired,
repoName: PropTypes.string.isRequired,
};
treeherder.component('similarJobsTab', react2angular(
SimilarJobsTab,
['repoName'],
['$injector']));

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

@ -68,9 +68,9 @@ import './js/controllers/tcjobactions';
import './plugins/tabs';
import './plugins/controller';
import './plugins/pinboard';
import './plugins/similar_jobs/controller';
import './details-panel/JobDetailsPane';
import './details-panel/FailureSummaryTab';
import './details-panel/AutoclassifyTab';
import './details-panel/AnnotationsTab';
import './details-panel/SimilarJobsTab';
import './js/filters';

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

@ -11,6 +11,17 @@ export const toDateStr = function toDateStr(timestamp) {
return new Date(timestamp * 1000).toLocaleString("en-US", dateFormat);
};
export const toShortDateStr = function toDateStr(timestamp) {
const dateFormat = {
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
hour12: false
};
return new Date(timestamp * 1000).toLocaleString("en-US", dateFormat);
};
// remove any words that are 1 letter long for matching
export const getSearchWords = function getHighlighterArray(text) {
const tokens = text.split(/[^a-zA-Z0-9_-]+/);

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

@ -93,8 +93,11 @@ export const getJobSearchStrHref = function getJobSearchStrHref(jobSearchStr) {
return `/#/jobs?${params.toString()}`;
};
export const jobsUrl = function getJobsUrl(tree, revision, jobId) {
return `/#/jobs?repo=${tree}&revision=${revision}&selectedJob=${jobId}`;
export const getJobsUrl = function getJobsUrl(params) {
const qs = Object.entries(params).reduce((acc, [key, value]) => (
[...acc, `${encodeURIComponent(key)}=${encodeURIComponent(value)}`]
), []).join('&');
return `/#/jobs?${qs}`;
};
export const bugsEndpoint = 'failures/';

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

@ -10,7 +10,7 @@ import { fetchBugData, updateDateRange, updateTreeName, updateSelectedBugDetails
import GenericTable from './GenericTable';
import GraphsContainer from './GraphsContainer';
import { updateQueryParams, calculateMetrics, prettyDate } from './helpers';
import { bugDetailsEndpoint, graphsEndpoint, parseQueryParams, createQueryParams, createApiUrl, jobsUrl,
import { bugDetailsEndpoint, graphsEndpoint, parseQueryParams, createQueryParams, createApiUrl, getJobsUrl,
getLogViewerUrl, bugzillaBugsApi } from '../helpers/urlHelper';
class BugDetailsView extends React.Component {
@ -84,7 +84,7 @@ class BugDetailsView extends React.Component {
{
Header: 'Revision',
accessor: 'revision',
Cell: props => <a href={jobsUrl(props.original.tree, props.value, props.original.job_id)} target="_blank">{props.value}</a>,
Cell: props => <a href={getJobsUrl({ repo: props.original.tree, revision: props.value, selectedJob: props.original.job_id })} target="_blank">{props.value}</a>,
},
{
Header: 'Platform',

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

@ -4,7 +4,7 @@ import angular from 'angular';
import perf from '../../perf';
import modifyAlertsCtrlTemplate from '../../../partials/perf/modifyalertsctrl.html';
import editAlertSummaryNotesCtrlTemplate from '../../../partials/perf/editnotesctrl.html';
import { getApiUrl } from "../../../helpers/urlHelper";
import { getApiUrl, getJobsUrl } from "../../../helpers/urlHelper";
import {
thDateFormat,
phTimeRanges,
@ -394,14 +394,6 @@ perf.controller('AlertsCtrl', [
});
};
function getJobsUrl(repo, fromChange, toChange) {
const urlParams = new URLSearchParams();
Object
.entries({ repo: repo, fromchange: fromChange, tochange: toChange })
.forEach(([k, v]) => { if (v) urlParams.append(k, v); });
return `index.html#/jobs?${urlParams.toString()}`;
}
function addAlertSummaries(alertSummaries, getMoreAlertSummariesHref) {
$scope.getMoreAlertSummariesHref = getMoreAlertSummariesHref;
@ -463,10 +455,10 @@ perf.controller('AlertsCtrl', [
if (summary.prevResultSetMetadata &&
summary.resultSetMetadata) {
summary.jobsURL = getJobsUrl(
summary.repository,
summary.prevResultSetMetadata.revision,
summary.resultSetMetadata.revision);
summary.jobsURL = getJobsUrl({
repo: summary.repository,
fromchange: summary.prevResultSetMetadata.revision,
tochange: summary.resultSetMetadata.revision });
summary.pushlogURL = repo.getPushLogHref({
from: summary.prevResultSetMetadata.revision,
to: summary.resultSetMetadata.revision

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

@ -548,7 +548,7 @@ treeherder.controller('PluginCtrl', [
const selectJobAndRender = function (job) {
$scope.jobLoadedPromise = selectJob(job);
$('#info-panel').addClass('info-panel-slide');
$scope.jobLoadedPromise.then(function () {
$scope.jobLoadedPromise.then(() => {
thTabs.showTab(thTabs.selectedTab, job.id);
});
};

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

@ -1,139 +0,0 @@
import $ from 'jquery';
import _ from 'lodash';
import angular from 'angular';
import treeherder from '../../js/treeherder';
import { getBtnClass, getStatus } from "../../helpers/jobHelper";
treeherder.controller('SimilarJobsPluginCtrl', [
'$scope', 'ThJobModel', 'ThTextLogStepModel',
'numberFilter', 'dateFilter', 'thClassificationTypes',
'ThResultSetModel', 'thNotify',
'thTabs',
function SimilarJobsPluginCtrl(
$scope, ThJobModel, ThTextLogStepModel,
numberFilter, dateFilter, thClassificationTypes,
ThResultSetModel, thNotify,
thTabs) {
// do the jobs retrieval based on the user selection
$scope.page_size = 20;
$scope.get_similar_jobs = function () {
thTabs.tabs.similarJobs.is_loading = true;
const options = {
count: $scope.page_size + 1,
offset: ($scope.page - 1) * $scope.page_size
};
angular.forEach($scope.similar_jobs_filters, function (value, key) {
if (value) {
options[key] = $scope.job[key];
}
});
ThJobModel.get_similar_jobs($scope.repoName, $scope.job.id, options)
.then(function (data) {
if (data.length > 0) {
if (data.length > $scope.page_size) {
$scope.has_next_page = true;
} else {
$scope.has_next_page = false;
}
data.pop();
// retrieve the list of result_set_ids
const result_set_ids = _.uniq(
_.map(data, 'result_set_id')
);
// get resultsets and revisions for the given ids
ThResultSetModel.getResultSetList(
$scope.repoName, result_set_ids, true
).then(function (response) {
//decorate the list of jobs with their result sets
const resultsets = _.keyBy(response.data.results, "id");
angular.forEach(data, function (obj) {
obj.result_set = resultsets[obj.result_set_id];
obj.revisionResultsetFilterUrl = $scope.urlBasePath + "?repo=" +
$scope.repoName + "&revision=" + obj.result_set.revisions[0].revision;
obj.authorResultsetFilterUrl = $scope.urlBasePath + "?repo=" +
$scope.repoName + "&author=" + encodeURIComponent(obj.result_set.author);
});
$scope.similar_jobs = $.merge($scope.similar_jobs, data);
// on the first page show the first element info by default
if ($scope.page === 1 && $scope.similar_jobs.length > 0) {
$scope.show_job_info($scope.similar_jobs[0]);
}
thTabs.tabs.similarJobs.is_loading = false;
},
function () {
thNotify.send("Error fetching pushes for similar jobs", "danger");
});
}
});
};
// update function triggered by the plugins controller
$scope.update_similar_jobs = function () {
if (angular.isDefined($scope.jobLoadedPromise)) {
$scope.jobLoadedPromise.then(function () {
$scope.similar_jobs = [];
$scope.page = 1;
$scope.similar_job_selected = null;
$scope.get_similar_jobs();
});
}
};
// expose the update function on the tab service
thTabs.tabs.similarJobs.update = $scope.update_similar_jobs;
$scope.similar_jobs = [];
$scope.similar_jobs_filters = {
machine_id: false,
build_platform_id: true,
option_collection_hash: true
};
$scope.button_class = job => (
getBtnClass(getStatus(job))
);
// this is triggered by the show more link
$scope.show_next = function () {
$scope.page += 1;
$scope.get_similar_jobs();
};
$scope.similar_job_selected = null;
$scope.show_job_info = function (job) {
ThJobModel.get($scope.repoName, job.id)
.then(function (job) {
$scope.similar_job_selected = job;
$scope.similar_job_selected.result_status = getStatus($scope.similar_job_selected);
let duration = (
$scope.similar_job_selected.end_timestamp - $scope.similar_job_selected.start_timestamp
)/60;
if (duration) {
duration = numberFilter(duration, 0);
}
$scope.similar_job_selected.duration = duration;
$scope.similar_job_selected.start_time = $scope.similar_job_selected.start_timestamp !== 0 ? dateFilter(
$scope.similar_job_selected.start_timestamp*1000,
'short'
) : "";
$scope.similar_job_selected.failure_classification = thClassificationTypes.classifications[
$scope.similar_job_selected.failure_classification_id
];
//retrieve the list of error lines
ThTextLogStepModel.query({
project: $scope.repoName,
jobId: $scope.similar_job_selected.id
}, function (textLogSteps) {
$scope.similar_job_selected.error_lines = _.flatten(
textLogSteps.map(s => s.errors));
});
});
};
}
]);

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

@ -1,125 +1,3 @@
<div class="similar_jobs" ng-controller="SimilarJobsPluginCtrl">
<div class="left_panel">
<table class="table table-super-condensed table-hover">
<thead>
<tr>
<th>Job</th><th>Pushed</th><th>Author</th><th>Revision</th>
</tr>
</thead>
<tbody>
<tr ng-click="show_job_info(similar_job)"
ng-class="{'table-active': similar_job_selected.id===similar_job.id}"
ng-repeat="similar_job in similar_jobs">
<td>
<button
class="btn btn-similar-jobs btn-xs"
ng-class="button_class(similar_job)">
{{ ::similar_job.job_type_symbol }}
<span ng-if="similar_job.failure_classification_id>1">
*
</span>
</button>
</td>
<td>{{ ::similar_job.result_set.push_timestamp*1000 | date:'EEE MMM d, H:mm:ss' }}</td>
<td>
<a href="{{ ::similar_job.authorResultsetFilterUrl }}">
{{ ::similar_job.result_set.author }}
</a>
</td>
<td>
<a href="{{ ::similar_job.revisionResultsetFilterUrl }}">
{{ ::similar_job.result_set.revisions[0].revision }}
</a>
</td>
</tr>
</tbody>
</table>
<button class="btn btn-light-bordered btn-sm" ng-if="has_next_page" href="" ng-click="show_next()">
Show previous jobs
</button>
</div>
<div class="right_panel">
<form role="form" class="form form-inline">
<div class="checkbox">
<input ng-change="update_similar_jobs()" type="checkbox" ng-model="similar_jobs_filters.build_platform_id"/>
<small>Same platform</small>
</div>
<div class="checkbox">
<input ng-change="update_similar_jobs()" type="checkbox" ng-model="similar_jobs_filters.option_collection_hash"/>
<small>Same options</small>
</div>
<div class="checkbox">
<input ng-change="update_similar_jobs()" type="checkbox" ng-model="similar_jobs_filters.machine_id"/>
<small>Same machine</small>
</div>
</form>
<div class="similar_job_detail">
<table class="table table-super-condensed" ng-if="similar_job_selected">
<tr>
<th>Result</th>
<td>{{ similar_job_selected.result_status }}</td>
</tr>
<tr>
<th>Machine name</th>
<td>
<a target="_blank" rel="noopener" href="{{ getSlaveHealthUrl(similar_job_selected.machine_name) }}">
{{ similar_job_selected.machine_name }}
</a>
</td>
</tr>
<tr>
<th>Build</th>
<td>
{{ similar_job_selected.build_architecture }} {{ similar_job_selected.build_platform }} {{ similar_job_selected.build_os }}
</td>
</tr>
<tr>
<th>Build option</th>
<td>
{{ similar_job_selected.platform_option }}
</td>
</tr>
<tr><th>Job name</th><td>{{ similar_job_selected.job_type_name }}</td></tr>
<tr>
<th>Started</th>
<td>
{{ similar_job_selected.start_timestamp*1000 | date:'EEE MMM d, H:mm:ss' }}
</td>
</tr>
<tr>
<th>Duration</th>
<td>
{{ similar_job_selected.duration >= 0 ? similar_job_selected.duration + ' minute(s)' : 'unknown' }}
</td>
</tr>
<tr>
<th>Classification</th>
<td>
<label class="badge {{ similar_job_selected.failure_classification.star }}">
{{ similar_job_selected.failure_classification.name }}
</label>
</td>
</tr>
<tr ng-if="similar_job_selected.error_lines.length > 0">
<td colspan=2>
<ul class="list-unstyled error_list">
<li class="" ng-repeat="error in similar_job_selected.error_lines">
<small title="{{::error.line}}">
{{::error.line}}
</small>
</li>
</ul>
</td>
</tr>
</table>
</div>
</div>
<div ng-if="tabs.similar_jobs.is_loading" class="overlay">
<div>
<span class="fa fa-spinner fa-pulse th-spinner-lg"></span>
</div>
</div>
</div>
<similar-jobs-tab
repo-name="repoName"
/>