зеркало из https://github.com/mozilla/treeherder.git
Bug 1450018 - Convert Similar jobs tab to ReactJS (#3455)
This commit is contained in:
Родитель
47ac771a84
Коммит
e09775179b
|
@ -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"
|
||||
/>
|
||||
|
|
Загрузка…
Ссылка в новой задаче