Rename details selectedJob to selectedJobFull (#5230)

* Rename details panel ``selectedJob`` to ``selectedJobFull``

The job that's passed in the DetailsPanel has a bunch of extra fields
that are not in the normal downloaded list of jobs.  So I wanted to
depict that ``selectedJob`` is not the same thing as what you see
in the DetailsPanel.

* Stop using Redux where not necessary

I was using Redux to assign the selectedJob in a few details
classes when I should have just passed it where it was needed.

* New addAggregateFields function

Instead of using a more heavy weight JobModel for each job,
we just persist some fields that were getting constantly calculated
over and over.  This was especially true during filtering and re-rendering.

* Remove some cruft leftover from Buildbot.
This commit is contained in:
Cameron Dawson 2019-08-06 12:10:33 -07:00 коммит произвёл GitHub
Родитель 9bf2dc7d91
Коммит 5d4ca44e7d
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
27 изменённых файлов: 419 добавлений и 450 удалений

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

@ -7,6 +7,7 @@ import { JobGroupComponent } from '../../../ui/job-view/pushes/JobGroup';
import FilterModel from '../../../ui/models/filter';
import mappedGroupFixture from '../mock/mappedGroup';
import mappedGroupDupsFixture from '../mock/mappedGroupDups';
import { addAggregateFields } from '../../../ui/helpers/job';
describe('JobGroup component', () => {
let countGroup;
@ -15,6 +16,11 @@ describe('JobGroup component', () => {
const filterModel = new FilterModel();
const pushGroupState = 'collapsed';
beforeAll(() => {
mappedGroupFixture.jobs.forEach(job => addAggregateFields(job));
mappedGroupDupsFixture.jobs.forEach(job => addAggregateFields(job));
});
beforeEach(() => {
countGroup = cloneDeep(mappedGroupFixture);
dupGroup = cloneDeep(mappedGroupDupsFixture);

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

@ -12,7 +12,8 @@ import FilterModel from '../../../ui/models/filter';
import { store } from '../../../ui/job-view/redux/store';
import { PinnedJobs } from '../../../ui/job-view/context/PinnedJobs';
import { getUrlParam, setUrlParam } from '../../../ui/helpers/location';
import JobModel from '../../../ui/models/job';
import platforms from '../mock/platforms';
import { addAggregateFields } from '../../../ui/helpers/job';
const testPush = {
id: 494796,
@ -34,170 +35,13 @@ const testPush = {
jobsLoaded: true,
};
const testPlatforms = [
{
name: 'Linux x64',
option: 'opt',
groups: [
{
name: 'Coverity Static Analysis',
tier: 2,
symbol: 'coverity',
mapKey: '494796coverity2linux64opt',
jobs: [
new JobModel({
build_architecture: '-',
build_os: '-',
build_platform: 'linux64',
build_platform_id: 106,
build_system_type: 'taskcluster',
end_timestamp: 1560356302,
failure_classification_id: 1,
id: 250970255,
job_group_description: '',
job_group_id: 947,
job_group_name: 'Coverity Static Analysis',
job_group_symbol: 'coverity',
job_guid: '2d180d39-8ac5-4200-995b-3f5c7b614596/0',
job_type_description: '',
job_type_id: 190421,
job_type_name: 'source-test-coverity-coverity',
job_type_symbol: 'cvsa',
last_modified: '2019-06-12T16:18:26.649628',
machine_name: 'i-0a2f82a56303c8ec2',
machine_platform_architecture: '-',
machine_platform_os: '-',
option_collection_hash: '102210fe594ee9b33d82058545b1ed14f4c8206e',
platform: 'linux64',
push_id: 494796,
reason: 'scheduled',
ref_data_name: '7542013e03efecbabf4b0bb931646f4fbff3a413',
result: 'success',
result_set_id: 494796,
signature: '7542013e03efecbabf4b0bb931646f4fbff3a413',
start_timestamp: 1560354928,
state: 'completed',
submit_timestamp: 1560354914,
tier: 2,
who: 'reviewbot@noreply.mozilla.org',
platform_option: 'opt',
visible: true,
selected: false,
}),
],
visible: true,
},
],
},
{
name: 'Gecko Decision Task',
option: 'opt',
groups: [
{
name: 'unknown',
tier: 1,
symbol: '',
mapKey: '4947961gecko-decisionopt',
jobs: [
new JobModel({
build_architecture: '-',
build_os: '-',
build_platform: 'gecko-decision',
build_platform_id: 107,
build_system_type: 'taskcluster',
end_timestamp: 1560354927,
failure_classification_id: 1,
id: 250970109,
job_group_description: '',
job_group_id: 2,
job_group_name: 'unknown',
job_group_symbol: '?',
job_guid: '7dd39d25-8990-44d2-8ba4-b2a3b319cc4d/0',
job_type_description: '',
job_type_id: 6689,
job_type_name: 'Gecko Decision Task',
job_type_symbol: 'D',
last_modified: '2019-06-12T15:55:29.549008',
machine_name: 'i-080c18493f1aa3d95',
machine_platform_architecture: '-',
machine_platform_os: '-',
option_collection_hash: '102210fe594ee9b33d82058545b1ed14f4c8206e',
platform: 'gecko-decision',
push_id: 494796,
reason: 'scheduled',
ref_data_name: '2aa083621bb989d6acf1151667288d5fe9616178',
result: 'success',
result_set_id: 494796,
signature: '2aa083621bb989d6acf1151667288d5fe9616178',
start_timestamp: 1560354846,
state: 'completed',
submit_timestamp: 1560354844,
tier: 1,
who: 'reviewbot@noreply.mozilla.org',
platform_option: 'opt',
visible: true,
selected: false,
}),
],
visible: true,
},
],
},
{
name: 'Linting',
option: 'opt',
groups: [
{
name: 'unknown',
tier: 1,
symbol: '',
mapKey: '4947961lintopt',
jobs: [
new JobModel({
build_architecture: '-',
build_os: '-',
build_platform: 'lint',
build_platform_id: 144,
build_system_type: 'taskcluster',
end_timestamp: 1560355013,
failure_classification_id: 1,
id: 250970251,
job_group_description: '',
job_group_id: 2,
job_group_name: 'unknown',
job_group_symbol: '?',
job_guid: '5df35b83-aff9-4ddf-b8c3-48eff52736f3/0',
job_type_description: '',
job_type_id: 114754,
job_type_name: 'source-test-mozlint-codespell',
job_type_symbol: 'spell',
last_modified: '2019-06-12T15:56:54.537683',
machine_name: 'i-081959e7fae55d041',
machine_platform_architecture: '-',
machine_platform_os: '-',
option_collection_hash: '102210fe594ee9b33d82058545b1ed14f4c8206e',
platform: 'lint',
push_id: 494796,
reason: 'scheduled',
ref_data_name: '6c2e8db7978ca4d5c0e38522552da4bc9b2e6b8b',
result: 'success',
result_set_id: 494796,
signature: '6c2e8db7978ca4d5c0e38522552da4bc9b2e6b8b',
start_timestamp: 1560354928,
state: 'completed',
submit_timestamp: 1560354914,
tier: 1,
who: 'reviewbot@noreply.mozilla.org',
platform_option: 'opt',
visible: true,
selected: false,
}),
],
visible: true,
},
],
},
];
beforeAll(() => {
platforms.forEach(platform =>
platform.groups.forEach(group =>
group.jobs.forEach(job => addAggregateFields(job)),
),
);
});
afterEach(() => {
cleanup();
@ -209,7 +53,7 @@ const testPushJobs = filterModel => (
<PinnedJobs>
<PushJobs
push={testPush}
platforms={testPlatforms}
platforms={platforms}
repoName="try"
filterModel={filterModel}
pushGroupState=""

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

@ -30,12 +30,17 @@ import {
fetchNextPushes,
updateRange,
} from '../../../../ui/job-view/redux/stores/pushes';
import { addAggregateFields } from '../../../../ui/helpers/job';
const mockStore = configureMockStore([thunk]);
describe('Pushes Redux store', () => {
const repoName = 'autoland';
beforeAll(() => {
Object.values(jobMap).forEach(job => addAggregateFields(job));
});
afterEach(() => {
cleanup();
fetchMock.reset();

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

@ -0,0 +1,164 @@
[
{
"name": "Linux x64",
"option": "opt",
"groups": [
{
"name": "Coverity Static Analysis",
"tier": 2,
"symbol": "coverity",
"mapKey": "494796coverity2linux64opt",
"jobs": [
{
"build_architecture": "-",
"build_os": "-",
"build_platform": "linux64",
"build_platform_id": 106,
"build_system_type": "taskcluster",
"end_timestamp": 1560356302,
"failure_classification_id": 1,
"id": 250970255,
"job_group_description": "",
"job_group_id": 947,
"job_group_name": "Coverity Static Analysis",
"job_group_symbol": "coverity",
"job_guid": "2d180d39-8ac5-4200-995b-3f5c7b614596/0",
"job_type_description": "",
"job_type_id": 190421,
"job_type_name": "source-test-coverity-coverity",
"job_type_symbol": "cvsa",
"last_modified": "2019-06-12T16:18:26.649628",
"machine_name": "i-0a2f82a56303c8ec2",
"machine_platform_architecture": "-",
"machine_platform_os": "-",
"option_collection_hash": "102210fe594ee9b33d82058545b1ed14f4c8206e",
"platform": "linux64",
"push_id": 494796,
"reason": "scheduled",
"ref_data_name": "7542013e03efecbabf4b0bb931646f4fbff3a413",
"result": "success",
"result_set_id": 494796,
"signature": "7542013e03efecbabf4b0bb931646f4fbff3a413",
"start_timestamp": 1560354928,
"state": "completed",
"submit_timestamp": 1560354914,
"tier": 2,
"who": "reviewbot@noreply.mozilla.org",
"platform_option": "opt",
"visible": true,
"selected": false
}
],
"visible": true
}
]
},
{
"name": "Gecko Decision Task",
"option": "opt",
"groups": [
{
"name": "unknown",
"tier": 1,
"symbol": "",
"mapKey": "4947961gecko-decisionopt",
"jobs": [
{
"build_architecture": "-",
"build_os": "-",
"build_platform": "gecko-decision",
"build_platform_id": 107,
"build_system_type": "taskcluster",
"end_timestamp": 1560354927,
"failure_classification_id": 1,
"id": 250970109,
"job_group_description": "",
"job_group_id": 2,
"job_group_name": "unknown",
"job_group_symbol": "?",
"job_guid": "7dd39d25-8990-44d2-8ba4-b2a3b319cc4d/0",
"job_type_description": "",
"job_type_id": 6689,
"job_type_name": "Gecko Decision Task",
"job_type_symbol": "D",
"last_modified": "2019-06-12T15:55:29.549008",
"machine_name": "i-080c18493f1aa3d95",
"machine_platform_architecture": "-",
"machine_platform_os": "-",
"option_collection_hash": "102210fe594ee9b33d82058545b1ed14f4c8206e",
"platform": "gecko-decision",
"push_id": 494796,
"reason": "scheduled",
"ref_data_name": "2aa083621bb989d6acf1151667288d5fe9616178",
"result": "success",
"result_set_id": 494796,
"signature": "2aa083621bb989d6acf1151667288d5fe9616178",
"start_timestamp": 1560354846,
"state": "completed",
"submit_timestamp": 1560354844,
"tier": 1,
"who": "reviewbot@noreply.mozilla.org",
"platform_option": "opt",
"visible": true,
"selected": false
}
],
"visible": true
}
]
},
{
"name": "Linting",
"option": "opt",
"groups": [
{
"name": "unknown",
"tier": 1,
"symbol": "",
"mapKey": "4947961lintopt",
"jobs": [
{
"build_architecture": "-",
"build_os": "-",
"build_platform": "lint",
"build_platform_id": 144,
"build_system_type": "taskcluster",
"end_timestamp": 1560355013,
"failure_classification_id": 1,
"id": 250970251,
"job_group_description": "",
"job_group_id": 2,
"job_group_name": "unknown",
"job_group_symbol": "?",
"job_guid": "5df35b83-aff9-4ddf-b8c3-48eff52736f3/0",
"job_type_description": "",
"job_type_id": 114754,
"job_type_name": "source-test-mozlint-codespell",
"job_type_symbol": "spell",
"last_modified": "2019-06-12T15:56:54.537683",
"machine_name": "i-081959e7fae55d041",
"machine_platform_architecture": "-",
"machine_platform_os": "-",
"option_collection_hash": "102210fe594ee9b33d82058545b1ed14f4c8206e",
"platform": "lint",
"push_id": 494796,
"reason": "scheduled",
"ref_data_name": "6c2e8db7978ca4d5c0e38522552da4bc9b2e6b8b",
"result": "success",
"result_set_id": 494796,
"signature": "6c2e8db7978ca4d5c0e38522552da4bc9b2e6b8b",
"start_timestamp": 1560354928,
"state": "completed",
"submit_timestamp": 1560354914,
"tier": 1,
"who": "reviewbot@noreply.mozilla.org",
"platform_option": "opt",
"visible": true,
"selected": false
}
],
"visible": true
}
]
}
]

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

@ -15,8 +15,6 @@ export const thMatchType = {
// choices available for the field filters
export const thFieldChoices = {
ref_data_name: { name: 'buildername/jobname', matchType: thMatchType.substr },
build_system_type: { name: 'build system', matchType: thMatchType.substr },
job_type_name: { name: 'job name', matchType: thMatchType.substr },
job_type_symbol: { name: 'job symbol', matchType: thMatchType.exactstr },
job_group_name: { name: 'group name', matchType: thMatchType.substr },

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

@ -22,14 +22,6 @@ const btnClasses = {
// failure classification ids that should be shown in "unclassified" mode
export const thUnclassifiedIds = [1, 7];
// The result will be unknown unless the state is complete, so much check both.
// TODO: We should consider storing either pending or running in the result,
// even when the job isn't complete. It would simplify a lot of UI code and
// I can't think of a reason that would hurt anything.
export const getStatus = function getStatus(job) {
return job.state === 'completed' ? job.result : job.state;
};
// Get the CSS class for job buttons as well as jobs that show in the pinboard.
// These also apply to result "groupings" like ``failures`` and ``in progress``
// for the colored filter chicklets on the nav bar.
@ -40,18 +32,12 @@ export const getBtnClass = function getBtnClass(
let btnClass = btnClasses[resultStatus] || 'btn-default';
// handle if a job is classified
// TODO: Check if the parseInt() is really needed here.
const classificationId = parseInt(failureClassificationId, 10);
if (classificationId > 1) {
if (failureClassificationId > 1) {
btnClass += '-classified';
}
return btnClass;
};
export const getJobBtnClass = function getJobBtnClass(job) {
return getBtnClass(getStatus(job), job.failure_classification_id);
};
export const isReftest = function isReftest(job) {
const {
job_group_name: gName,
@ -203,22 +189,54 @@ export const findJobInstance = function findJobInstance(jobId, scrollTo) {
}
};
export const getSearchStr = function getSearchStr(job) {
export const addAggregateFields = function addAggregateFields(job) {
const {
job_group_name,
job_group_symbol,
job_type_name,
job_type_symbol,
state,
result,
platform,
platform_option,
signature,
duration,
failure_classification_id,
submit_timestamp,
start_timestamp,
end_timestamp,
} = job;
job.resultStatus = state === 'completed' ? result : state;
// we want to join the group and type information together
// so we can search for it as one token (useful when
// we want to do a search on something like `fxup-esr(`)
const symbolInfo = job.job_group_symbol === '?' ? '' : job.job_group_symbol;
const symbolInfo = job_group_symbol === '?' ? '' : job_group_symbol;
return [
thPlatformMap[job.platform] || job.platform,
job.platform_option,
job.job_group_name === 'unknown' ? undefined : job.job_group_name,
job.job_type_name,
`${symbolInfo}(${job.job_type_symbol})`,
job.title = [
thPlatformMap[platform] || platform,
platform_option,
job_group_name === 'unknown' ? undefined : job_group_name,
job_type_name,
`${symbolInfo}(${job_type_symbol})`,
]
.filter(item => typeof item !== 'undefined')
.join(' ')
.toLowerCase();
job.searchStr = `${job.title} ${signature}`;
if (!duration) {
// If start time is 0, then duration should be from requesttime to now
// If we have starttime and no endtime, then duration should be starttime to now
// If we have both starttime and endtime, then duration will be between those two
const endtime = end_timestamp || Date.now() / 1000;
const starttime = start_timestamp || submit_timestamp;
job.duration = Math.round((endtime - starttime) / 60, 0);
}
job.hoverText = `${job_type_name} - ${job.resultStatus} - ${job.duration} mins`;
job.btnClass = getBtnClass(job.resultStatus, failure_classification_id);
return job;
};
export const getJobSearchStrHref = function getJobSearchStrHref(jobSearchStr) {
@ -227,9 +245,3 @@ export const getJobSearchStrHref = function getJobSearchStrHref(jobSearchStr) {
return `${uiJobsUrlBase}?${params.toString()}`;
};
export const getHoverText = function getHoverText(job) {
const duration = Math.round((job.end_timestamp - job.start_timestamp) / 60);
return `${job.job_type_name} - ${getStatus(job)} - ${duration} mins`;
};

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

@ -5,6 +5,7 @@ import { connect } from 'react-redux';
import { thEvents, thBugSuggestionLimit } from '../../helpers/constants';
import { withPinnedJobs } from '../context/PinnedJobs';
import { addAggregateFields } from '../../helpers/job';
import { getLogViewerUrl, getReftestUrl } from '../../helpers/url';
import BugJobMapModel from '../../models/bugJobMap';
import BugSuggestionsModel from '../../models/bugSuggestions';
@ -29,6 +30,7 @@ class DetailsPanel extends React.Component {
this.selectJobController = null;
this.state = {
selectedJobFull: null,
jobDetails: [],
jobLogUrls: [],
jobDetailLoading: false,
@ -182,7 +184,7 @@ class DetailsPanel extends React.Component {
);
const jobDetailPromise = JobDetailModel.getJobDetails(
{ job_guid: selectedJob.job_guid },
{ job_id: selectedJob.id },
this.selectJobController.signal,
);
@ -211,22 +213,12 @@ class DetailsPanel extends React.Component {
// This version of the job has more information than what we get in the main job list. This
// is what we'll pass to the rest of the details panel. It has extra fields like
// taskcluster_metadata.
Object.assign(selectedJob, jobResult);
const selectedJobFull = jobResult;
const jobRevision = push.revision;
jobDetails = jobDetailResult;
addAggregateFields(selectedJobFull);
// incorporate the buildername into the job details if this is a buildbot job
// (i.e. it has a buildbot request id)
const buildbotRequestIdDetail = jobDetails.find(
detail => detail.title === 'buildbot_request_id',
);
if (buildbotRequestIdDetail) {
jobDetails = [
...jobDetails,
{ title: 'Buildername', value: selectedJob.ref_data_name },
];
}
jobDetails = jobDetailResult;
// the third result comes from the jobLogUrl promise
// exclude the json log URLs
@ -292,6 +284,7 @@ class DetailsPanel extends React.Component {
this.setState(
{
selectedJobFull,
jobLogUrls,
jobDetails,
logParseStatus,
@ -325,9 +318,9 @@ class DetailsPanel extends React.Component {
classificationMap,
classificationTypes,
isPinBoardVisible,
selectedJob,
} = this.props;
const {
selectedJobFull,
jobDetails,
jobRevision,
jobLogUrls,
@ -351,19 +344,19 @@ class DetailsPanel extends React.Component {
<div
id="details-panel"
style={{ height: `${detailsPanelHeight}px` }}
className={selectedJob ? 'details-panel-slide' : 'hidden'}
className={selectedJobFull ? 'details-panel-slide' : 'hidden'}
>
<PinBoard
repoName={repoName}
currentRepo={currentRepo}
isLoggedIn={user.isLoggedIn || false}
classificationTypes={classificationTypes}
selectedJob={selectedJob}
selectedJobFull={selectedJobFull}
/>
{!!selectedJob && (
{!!selectedJobFull && (
<div id="details-panel-content">
<SummaryPanel
selectedJob={selectedJob}
selectedJobFull={selectedJobFull}
repoName={repoName}
currentRepo={currentRepo}
classificationMap={classificationMap}
@ -380,7 +373,7 @@ class DetailsPanel extends React.Component {
/>
<span className="job-tabs-divider" />
<TabsPanel
selectedJob={selectedJob}
selectedJobFull={selectedJobFull}
jobDetails={jobDetails}
perfJobDetail={perfJobDetail}
repoName={repoName}

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

@ -7,11 +7,7 @@ import { faPlusSquare, faTimes } from '@fortawesome/free-solid-svg-icons';
import { thEvents } from '../../helpers/constants';
import { formatModelError } from '../../helpers/errorMessage';
import {
getJobBtnClass,
getHoverText,
findJobInstance,
} from '../../helpers/job';
import { findJobInstance } from '../../helpers/job';
import { isSHAorCommit } from '../../helpers/revision';
import { getBugUrl } from '../../helpers/url';
import BugJobMapModel from '../../models/bugJobMap';
@ -356,7 +352,7 @@ class PinBoard extends React.Component {
render() {
const {
selectedJob,
selectedJobFull,
revisionTips,
isLoggedIn,
isPinBoardVisible,
@ -372,7 +368,7 @@ class PinBoard extends React.Component {
failureClassificationComment,
} = this.props;
const { enteringBugNumber, newBugNumber } = this.state;
const selectedJobId = selectedJob ? selectedJob.id : null;
const selectedJobId = selectedJobFull ? selectedJobFull.id : null;
return (
<div id="pinboard-panel" className={isPinBoardVisible ? '' : 'hidden'}>
@ -387,12 +383,12 @@ class PinBoard extends React.Component {
{Object.values(pinnedJobs).map(job => (
<span className="btn-group" key={job.id}>
<span
className={`btn pinned-job ${getJobBtnClass(job)} ${
className={`btn pinned-job ${job.btnClass} ${
selectedJobId === job.id
? 'btn-lg selected-job'
: 'btn-xs'
}`}
title={getHoverText(job)}
title={job.hoverText}
onClick={() => setSelectedJob(job)}
data-job-id={job.job_id}
>
@ -644,13 +640,13 @@ PinBoard.propTypes = {
currentRepo: PropTypes.object.isRequired,
failureClassificationId: PropTypes.number.isRequired,
failureClassificationComment: PropTypes.string.isRequired,
selectedJob: PropTypes.object,
selectedJobFull: PropTypes.object,
email: PropTypes.string,
revisionTips: PropTypes.array,
};
PinBoard.defaultProps = {
selectedJob: null,
selectedJobFull: null,
email: null,
revisionTips: [],
};

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

@ -71,21 +71,23 @@ class ActionBar extends React.PureComponent {
};
canCancel = () => {
const { selectedJob } = this.props;
return selectedJob.state === 'pending' || selectedJob.state === 'running';
const { selectedJobFull } = this.props;
return (
selectedJobFull.state === 'pending' || selectedJobFull.state === 'running'
);
};
createGeckoProfile = async () => {
const { user, selectedJob, notify } = this.props;
const { user, selectedJobFull, notify } = this.props;
if (!user.isLoggedIn) {
return notify('Must be logged in to create a gecko profile', 'danger');
}
const { id: decisionTaskId } = await PushModel.getDecisionTaskId(
selectedJob.push_id,
selectedJobFull.push_id,
notify,
);
TaskclusterModel.load(decisionTaskId, selectedJob).then(results => {
TaskclusterModel.load(decisionTaskId, selectedJobFull).then(results => {
const geckoprofile = results.actions.find(
result => result.name === 'geckoprofile',
);
@ -141,7 +143,7 @@ class ActionBar extends React.PureComponent {
};
backfillJob = async () => {
const { user, selectedJob, notify } = this.props;
const { user, selectedJobFull, notify } = this.props;
if (!this.canBackfill()) {
return;
@ -153,21 +155,21 @@ class ActionBar extends React.PureComponent {
return;
}
if (!selectedJob.id) {
if (!selectedJobFull.id) {
notify('Job not yet loaded for backfill', 'warning');
return;
}
if (
selectedJob.build_system_type === 'taskcluster' ||
selectedJob.reason.startsWith('Created by BBB for task')
selectedJobFull.build_system_type === 'taskcluster' ||
selectedJobFull.reason.startsWith('Created by BBB for task')
) {
const { id: decisionTaskId } = await PushModel.getDecisionTaskId(
selectedJob.push_id,
selectedJobFull.push_id,
notify,
);
TaskclusterModel.load(decisionTaskId, selectedJob).then(results => {
TaskclusterModel.load(decisionTaskId, selectedJobFull).then(results => {
const backfilltask = results.actions.find(
result => result.name === 'backfill',
);
@ -195,13 +197,13 @@ class ActionBar extends React.PureComponent {
};
isolateJob = async () => {
const { user, selectedJob, notify } = this.props;
const { user, selectedJobFull, notify } = this.props;
const { id: decisionTaskId } = await PushModel.getDecisionTaskId(
selectedJob.push_id,
selectedJobFull.push_id,
notify,
);
if (!isTestIsolatable(selectedJob)) {
if (!isTestIsolatable(selectedJobFull)) {
return;
}
@ -211,23 +213,23 @@ class ActionBar extends React.PureComponent {
return;
}
if (!selectedJob.id) {
if (!selectedJobFull.id) {
notify('Job not yet loaded for isolation', 'warning');
return;
}
if (selectedJob.state !== 'completed') {
if (selectedJobFull.state !== 'completed') {
notify('Job not yet completed. Try again later.', 'warning');
return;
}
if (
selectedJob.build_system_type === 'taskcluster' ||
selectedJob.reason.startsWith('Created by BBB for task')
selectedJobFull.build_system_type === 'taskcluster' ||
selectedJobFull.reason.startsWith('Created by BBB for task')
) {
TaskclusterModel.load(decisionTaskId, selectedJob).then(results => {
TaskclusterModel.load(decisionTaskId, selectedJobFull).then(results => {
const isolationtask = results.actions.find(
result => result.name === 'isolate-test-failures',
);
@ -316,8 +318,8 @@ class ActionBar extends React.PureComponent {
};
createInteractiveTask = async () => {
const { user, selectedJob, repoName, notify } = this.props;
const jobId = selectedJob.id;
const { user, selectedJobFull, repoName, notify } = this.props;
const jobId = selectedJobFull.id;
if (!user.isLoggedIn) {
return notify(
@ -373,7 +375,7 @@ class ActionBar extends React.PureComponent {
};
cancelJob = () => {
this.cancelJobs([this.props.selectedJob]);
this.cancelJobs([this.props.selectedJobFull]);
};
toggleCustomJobActions = () => {
@ -384,7 +386,7 @@ class ActionBar extends React.PureComponent {
render() {
const {
selectedJob,
selectedJobFull,
logViewerUrl,
logViewerFullUrl,
jobLogUrls,
@ -407,7 +409,7 @@ class ActionBar extends React.PureComponent {
id="pin-job-btn"
title="Add this job to the pinboard"
className="btn icon-blue"
onClick={() => pinJob(selectedJob)}
onClick={() => pinJob(selectedJobFull)}
>
<FontAwesomeIcon icon={faThumbtack} title="Pin job" />
</Button>
@ -422,12 +424,12 @@ class ActionBar extends React.PureComponent {
}
className={`btn ${user.isLoggedIn ? 'icon-green' : 'disabled'}`}
disabled={!user.isLoggedIn}
onClick={() => this.retriggerJob([selectedJob])}
onClick={() => this.retriggerJob([selectedJobFull])}
>
<FontAwesomeIcon icon={faRedo} title="Retrigger job" />
</Button>
</li>
{isReftest(selectedJob) &&
{isReftest(selectedJobFull) &&
jobLogUrls.map(jobLogUrl => (
<li key={`reftest-${jobLogUrl.id}`}>
<a
@ -484,7 +486,7 @@ class ActionBar extends React.PureComponent {
Backfill
</span>
</li>
{selectedJob.taskcluster_metadata && (
{selectedJobFull.taskcluster_metadata && (
<React.Fragment>
<li>
<a
@ -492,7 +494,7 @@ class ActionBar extends React.PureComponent {
rel="noopener noreferrer"
className="dropdown-item"
href={getInspectTaskUrl(
selectedJob.taskcluster_metadata.task_id,
selectedJobFull.taskcluster_metadata.task_id,
)}
>
Inspect Task
@ -506,7 +508,7 @@ class ActionBar extends React.PureComponent {
Create Interactive Task
</Button>
</li>
{isPerfTest(selectedJob) && (
{isPerfTest(selectedJobFull) && (
<li>
<Button
className="dropdown-item py-2"
@ -516,7 +518,7 @@ class ActionBar extends React.PureComponent {
</Button>
</li>
)}
{isTestIsolatable(selectedJob) && (
{isTestIsolatable(selectedJobFull) && (
<li>
<Button
className="dropdown-item py-2"
@ -542,8 +544,8 @@ class ActionBar extends React.PureComponent {
</nav>
{customJobActionsShowing && (
<CustomJobActions
job={selectedJob}
pushId={selectedJob.push_id}
job={selectedJobFull}
pushId={selectedJobFull.push_id}
isLoggedIn={user.isLoggedIn}
toggle={this.toggleCustomJobActions}
/>
@ -557,7 +559,7 @@ ActionBar.propTypes = {
pinJob: PropTypes.func.isRequired,
user: PropTypes.object.isRequired,
repoName: PropTypes.string.isRequired,
selectedJob: PropTypes.object.isRequired,
selectedJobFull: PropTypes.object.isRequired,
logParseStatus: PropTypes.string.isRequired,
notify: PropTypes.func.isRequired,
jobLogUrls: PropTypes.array,

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

@ -1,28 +1,26 @@
import React from 'react';
import PropTypes from 'prop-types';
import { getStatus } from '../../../helpers/job';
function StatusPanel(props) {
const { selectedJob } = props;
const shadingClass = `result-status-shading-${getStatus(selectedJob)}`;
const { selectedJobFull } = props;
const shadingClass = `result-status-shading-${selectedJobFull.resultStatus}`;
return (
<li id="result-status-pane" className={`small ${shadingClass}`}>
<div>
<strong>Result:</strong>
<span> {selectedJob.result}</span>
<span> {selectedJobFull.result}</span>
</div>
<div>
<strong>State:</strong>
<span> {selectedJob.state}</span>
<span> {selectedJobFull.state}</span>
</div>
</li>
);
}
StatusPanel.propTypes = {
selectedJob: PropTypes.object.isRequired,
selectedJobFull: PropTypes.object.isRequired,
};
export default StatusPanel;

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

@ -13,7 +13,7 @@ class SummaryPanel extends React.PureComponent {
render() {
const {
repoName,
selectedJob,
selectedJobFull,
latestClassification,
bugs,
jobLogUrls,
@ -38,7 +38,7 @@ class SummaryPanel extends React.PureComponent {
return (
<div id="summary-panel" role="region" aria-label="Summary">
<ActionBar
selectedJob={selectedJob}
selectedJobFull={selectedJobFull}
repoName={repoName}
logParseStatus={logParseStatus}
isTryRepo={currentRepo.is_try_repo}
@ -65,15 +65,15 @@ class SummaryPanel extends React.PureComponent {
<ul className="list-unstyled">
{latestClassification && (
<ClassificationsPanel
job={selectedJob}
job={selectedJobFull}
classification={latestClassification}
classificationMap={classificationMap}
bugs={bugs}
currentRepo={currentRepo}
/>
)}
<StatusPanel selectedJob={selectedJob} />
<JobInfo job={selectedJob} extraFields={logStatus} />
<StatusPanel selectedJobFull={selectedJobFull} />
<JobInfo job={selectedJobFull} extraFields={logStatus} />
</ul>
</div>
</div>
@ -88,7 +88,7 @@ SummaryPanel.propTypes = {
user: PropTypes.object.isRequired,
currentRepo: PropTypes.object.isRequired,
classificationMap: PropTypes.object.isRequired,
selectedJob: PropTypes.object.isRequired,
selectedJobFull: PropTypes.object.isRequired,
latestClassification: PropTypes.object,
jobLogUrls: PropTypes.array,
jobDetailLoading: PropTypes.bool,

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

@ -175,9 +175,13 @@ class AnnotationsTab extends React.Component {
};
deleteClassification = classification => {
const { selectedJob, recalculateUnclassifiedCounts, notify } = this.props;
const {
selectedJobFull,
recalculateUnclassifiedCounts,
notify,
} = this.props;
selectedJob.failure_classification_id = 1;
selectedJobFull.failure_classification_id = 1;
recalculateUnclassifiedCounts();
classification.destroy().then(
@ -247,16 +251,10 @@ AnnotationsTab.propTypes = {
classifications: PropTypes.array.isRequired,
recalculateUnclassifiedCounts: PropTypes.func.isRequired,
notify: PropTypes.func.isRequired,
selectedJob: PropTypes.object,
selectedJobFull: PropTypes.object.isRequired,
};
AnnotationsTab.defaultProps = {
selectedJob: null,
};
const mapStateToProps = ({ selectedJob: { selectedJob } }) => ({ selectedJob });
export default connect(
mapStateToProps,
null,
{ notify, recalculateUnclassifiedCounts },
)(AnnotationsTab);

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

@ -6,7 +6,7 @@ import { faSpinner } from '@fortawesome/free-solid-svg-icons';
import { thMaxPushFetchSize } from '../../../helpers/constants';
import { toDateStr, toShortDateStr } from '../../../helpers/display';
import { getBtnClass, getStatus } from '../../../helpers/job';
import { addAggregateFields } from '../../../helpers/job';
import { getJobsUrl } from '../../../helpers/url';
import JobModel from '../../../models/job';
import PushModel from '../../../models/push';
@ -42,7 +42,7 @@ class SimilarJobsTab extends React.Component {
getSimilarJobs = async () => {
const { page, similarJobs, selectedSimilarJob } = this.state;
const { repoName, selectedJob, notify } = this.props;
const { repoName, selectedJobFull, notify } = this.props;
const options = {
// get one extra to detect if there are more jobs that can be loaded (hasNextPage)
count: this.pageSize + 1,
@ -52,14 +52,14 @@ class SimilarJobsTab extends React.Component {
['filterBuildPlatformId', 'filterOptionCollectionHash'].forEach(key => {
if (this.state[key]) {
const field = this.filterMap[key];
options[field] = selectedJob[field];
options[field] = selectedJobFull[field];
}
});
const {
data: newSimilarJobs,
failureStatus,
} = await JobModel.getSimilarJobs(selectedJob.id, options);
} = await JobModel.getSimilarJobs(selectedJobFull.id, options);
if (!failureStatus) {
this.setState({ hasNextPage: newSimilarJobs.length > this.pageSize });
@ -119,8 +119,7 @@ class SimilarJobsTab extends React.Component {
const { repoName, classificationMap } = this.props;
JobModel.get(repoName, job.id).then(nextJob => {
nextJob.result_status = getStatus(nextJob);
nextJob.duration = (nextJob.end_timestamp - nextJob.start_timestamp) / 60;
addAggregateFields(nextJob);
nextJob.failure_classification =
classificationMap[nextJob.failure_classification_id];
@ -155,7 +154,6 @@ class SimilarJobsTab extends React.Component {
filterBuildPlatformId,
isLoading,
} = this.state;
const button_class = job => getBtnClass(getStatus(job));
const selectedSimilarJobId = selectedSimilarJob
? selectedSimilarJob.id
: null;
@ -187,9 +185,7 @@ class SimilarJobsTab extends React.Component {
>
<td>
<button
className={`btn btn-similar-jobs btn-xs ${button_class(
similarJob,
)}`}
className={`btn btn-similar-jobs btn-xs ${similarJob.btnClass}`}
type="button"
>
{similarJob.job_type_symbol}
@ -250,7 +246,7 @@ class SimilarJobsTab extends React.Component {
<tbody>
<tr>
<th>Result</th>
<td>{selectedSimilarJob.result_status}</td>
<td>{selectedSimilarJob.resultStatus}</td>
</tr>
<tr>
<th>Build</th>
@ -329,16 +325,10 @@ SimilarJobsTab.propTypes = {
repoName: PropTypes.string.isRequired,
classificationMap: PropTypes.object.isRequired,
notify: PropTypes.func.isRequired,
selectedJob: PropTypes.object,
selectedJobFull: PropTypes.object.isRequired,
};
SimilarJobsTab.defaultProps = {
selectedJob: null,
};
const mapStateToProps = ({ selectedJob: { selectedJob } }) => ({ selectedJob });
export default connect(
mapStateToProps,
null,
{ notify },
)(SimilarJobsTab);

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

@ -11,7 +11,6 @@ import {
} from '@fortawesome/free-solid-svg-icons';
import { thEvents } from '../../../helpers/constants';
import { getStatus } from '../../../helpers/job';
import JobDetails from '../../../shared/JobDetails';
import { withPinnedJobs } from '../../context/PinnedJobs';
import { clearSelectedJob } from '../../redux/stores/selectedJob';
@ -31,23 +30,23 @@ class TabsPanel extends React.Component {
}
static getDerivedStateFromProps(props, state) {
const { perfJobDetail, selectedJob } = props;
const { perfJobDetail, selectedJobFull } = props;
// This fires every time the props change. But we only want to figure out the new default
// tab when we get a new job. However, the job could change, then later, the perf details fetch
// returns. So we need to check for a change in the size of the perfJobDetail too.
if (
state.jobId !== selectedJob.id ||
state.jobId !== selectedJobFull.id ||
state.perfJobDetailSize !== perfJobDetail.length
) {
const tabIndex = TabsPanel.getDefaultTabIndex(
getStatus(selectedJob),
selectedJobFull.resultStatus,
!!perfJobDetail.length,
);
return {
tabIndex,
jobId: selectedJob.id,
jobId: selectedJobFull.id,
perfJobDetailSize: perfJobDetail.length,
};
}
@ -115,6 +114,7 @@ class TabsPanel extends React.Component {
logViewerFullUrl,
reftestUrl,
clearSelectedJob,
selectedJobFull,
} = this.props;
const { tabIndex } = this.state;
@ -192,6 +192,7 @@ class TabsPanel extends React.Component {
logParseStatus={logParseStatus}
logViewerFullUrl={logViewerFullUrl}
reftestUrl={reftestUrl}
selectedJobFull={selectedJobFull}
/>
</TabPanel>
<TabPanel>
@ -199,12 +200,14 @@ class TabsPanel extends React.Component {
classificationMap={classificationMap}
classifications={classifications}
bugs={bugs}
selectedJobFull={selectedJobFull}
/>
</TabPanel>
<TabPanel>
<SimilarJobsTab
repoName={repoName}
classificationMap={classificationMap}
selectedJobFull={selectedJobFull}
/>
</TabPanel>
{!!perfJobDetail.length && (
@ -232,9 +235,9 @@ TabsPanel.propTypes = {
countPinnedJobs: PropTypes.number.isRequired,
bugs: PropTypes.array.isRequired,
clearSelectedJob: PropTypes.func.isRequired,
selectedJobFull: PropTypes.object.isRequired,
perfJobDetail: PropTypes.array,
suggestions: PropTypes.array,
selectedJob: PropTypes.object,
jobRevision: PropTypes.string,
errors: PropTypes.array,
bugSuggestionsLoading: PropTypes.bool,
@ -246,7 +249,6 @@ TabsPanel.propTypes = {
TabsPanel.defaultProps = {
suggestions: [],
selectedJob: null,
errors: [],
bugSuggestionsLoading: false,
jobLogUrls: [],

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

@ -1,6 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import Highlighter from 'react-highlight-words';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faThumbtack } from '@fortawesome/free-solid-svg-icons';
@ -10,7 +9,14 @@ import { getBugUrl } from '../../../../helpers/url';
import { withPinnedJobs } from '../../../context/PinnedJobs';
function BugListItem(props) {
const { bug, suggestion, bugClassName, title, selectedJob, addBug } = props;
const {
bug,
suggestion,
bugClassName,
title,
selectedJobFull,
addBug,
} = props;
const bugUrl = getBugUrl(bug.id);
return (
@ -18,7 +24,7 @@ function BugListItem(props) {
<button
className="btn btn-xs btn-light-bordered"
type="button"
onClick={() => addBug(bug, selectedJob)}
onClick={() => addBug(bug, selectedJobFull)}
title="add to list of bugs to associate with all pinned jobs"
>
<FontAwesomeIcon icon={faThumbtack} title="Select bug" />
@ -47,7 +53,7 @@ BugListItem.propTypes = {
bug: PropTypes.object.isRequired,
suggestion: PropTypes.object.isRequired,
addBug: PropTypes.func.isRequired,
selectedJob: PropTypes.object.isRequired,
selectedJobFull: PropTypes.object.isRequired,
bugClassName: PropTypes.string,
title: PropTypes.string,
};
@ -57,6 +63,4 @@ BugListItem.defaultProps = {
title: null,
};
const mapStateToProps = ({ selectedJob: { selectedJob } }) => ({ selectedJob });
export default connect(mapStateToProps)(withPinnedJobs(BugListItem));
export default withPinnedJobs(BugListItem);

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

@ -1,6 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
@ -24,9 +23,9 @@ class FailureSummaryTab extends React.Component {
}
fileBug = suggestion => {
const { selectedJob, pinJob } = this.props;
const { selectedJobFull, pinJob } = this.props;
pinJob(selectedJob);
pinJob(selectedJobFull);
this.setState({
isBugFilerOpen: true,
suggestion,
@ -54,7 +53,7 @@ class FailureSummaryTab extends React.Component {
errors,
logViewerFullUrl,
bugSuggestionsLoading,
selectedJob,
selectedJobFull,
reftestUrl,
} = this.props;
const { isBugFilerOpen, suggestion } = this.state;
@ -70,6 +69,7 @@ class FailureSummaryTab extends React.Component {
index={index}
suggestion={suggestion}
toggleBugFiler={() => this.fileBug(suggestion)}
selectedJobFull={selectedJobFull}
/>
))}
@ -150,9 +150,9 @@ class FailureSummaryTab extends React.Component {
suggestions={suggestions}
fullLog={jobLogUrls[0].url}
parsedLog={logViewerFullUrl}
reftestUrl={isReftest(selectedJob) ? reftestUrl : ''}
reftestUrl={isReftest(selectedJobFull) ? reftestUrl : ''}
successCallback={this.bugFilerCallback}
jobGroupName={selectedJob.job_group_name}
jobGroupName={selectedJobFull.job_group_name}
/>
)}
</div>
@ -163,8 +163,8 @@ class FailureSummaryTab extends React.Component {
FailureSummaryTab.propTypes = {
addBug: PropTypes.func.isRequired,
pinJob: PropTypes.func.isRequired,
selectedJobFull: PropTypes.object.isRequired,
suggestions: PropTypes.array,
selectedJob: PropTypes.object,
errors: PropTypes.array,
bugSuggestionsLoading: PropTypes.bool,
jobLogUrls: PropTypes.array,
@ -175,7 +175,6 @@ FailureSummaryTab.propTypes = {
FailureSummaryTab.defaultProps = {
suggestions: [],
selectedJob: null,
reftestUrl: null,
errors: [],
bugSuggestionsLoading: false,
@ -184,6 +183,4 @@ FailureSummaryTab.defaultProps = {
logViewerFullUrl: null,
};
const mapStateToProps = ({ selectedJob: { selectedJob } }) => ({ selectedJob });
export default connect(mapStateToProps)(withPinnedJobs(FailureSummaryTab));
export default withPinnedJobs(FailureSummaryTab);

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

@ -22,7 +22,7 @@ export default class SuggestionsListItem extends React.Component {
};
render() {
const { suggestion, toggleBugFiler } = this.props;
const { suggestion, toggleBugFiler, selectedJobFull } = this.props;
const { suggestionShowMore } = this.state;
return (
@ -42,7 +42,12 @@ export default class SuggestionsListItem extends React.Component {
{suggestion.valid_open_recent && (
<ul className="list-unstyled failure-summary-bugs">
{suggestion.bugs.open_recent.map(bug => (
<BugListItem key={bug.id} bug={bug} suggestion={suggestion} />
<BugListItem
key={bug.id}
bug={bug}
suggestion={suggestion}
selectedJobFull={selectedJobFull}
/>
))}
</ul>
)}
@ -68,6 +73,7 @@ export default class SuggestionsListItem extends React.Component {
suggestion={suggestion}
bugClassName={bug.resolution !== '' ? 'strike-through' : ''}
title={bug.resolution !== '' ? bug.resolution : ''}
selectedJobFull={selectedJobFull}
/>
))}
</ul>
@ -87,6 +93,7 @@ export default class SuggestionsListItem extends React.Component {
}
SuggestionsListItem.propTypes = {
selectedJobFull: PropTypes.object.isRequired,
suggestion: PropTypes.object.isRequired,
toggleBugFiler: PropTypes.func.isRequired,
};

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

@ -4,7 +4,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faStar as faStarRegular } from '@fortawesome/free-regular-svg-icons';
import { faStar as faStarSolid } from '@fortawesome/free-solid-svg-icons';
import { getBtnClass, findJobInstance } from '../../helpers/job';
import { findJobInstance } from '../../helpers/job';
import { getSelectedJobId, getUrlParam } from '../../helpers/location';
export default class JobButtonComponent extends React.Component {
@ -80,24 +80,19 @@ export default class JobButtonComponent extends React.Component {
}
render() {
const { job, resultStatus } = this.props;
const { job } = this.props;
const { isSelected, isRunnableSelected } = this.state;
const {
state,
job_type_name,
failure_classification_id,
end_timestamp,
start_timestamp,
ref_data_name,
visible,
id,
job_type_symbol,
btnClass,
} = job;
if (!visible) return null;
const runnable = state === 'runnable';
const btnClass = getBtnClass(resultStatus, failure_classification_id);
let title = `${resultStatus} | ${job_type_name}`;
let classifiedIcon = null;
if (failure_classification_id > 1) {
@ -105,20 +100,14 @@ export default class JobButtonComponent extends React.Component {
failure_classification_id === 7 ? faStarRegular : faStarSolid;
}
if (state === 'completed') {
const duration = Math.round((end_timestamp - start_timestamp) / 60);
title += ` (${duration} mins)`;
}
const classes = ['btn', btnClass, 'filter-shown'];
const attributes = {
'data-job-id': id,
title,
title: job.hoverText,
};
if (runnable) {
classes.push('runnable-job-btn', 'runnable');
attributes['data-buildername'] = ref_data_name;
if (isRunnableSelected) {
classes.push('runnable-job-btn-selected');
}

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

@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
import countBy from 'lodash/countBy';
import { thFailureResults } from '../../helpers/constants';
import { getBtnClass, getStatus } from '../../helpers/job';
import { getSelectedJobId, getUrlParam } from '../../helpers/location';
import JobButton from './JobButton';
@ -72,14 +71,15 @@ export class JobGroupComponent extends React.Component {
const stateCounts = {};
const typeSymbolCounts = countBy(jobs, 'job_type_symbol');
jobs.forEach(job => {
if (!job.visible) return;
const status = getStatus(job);
const { resultStatus, visible, btnClass } = job;
if (!visible) return;
let countInfo = {
btnClass: getBtnClass(status, job.failure_classification_id),
countText: status,
btnClass,
countText: resultStatus,
};
if (
thFailureResults.includes(status) ||
thFailureResults.includes(resultStatus) ||
(typeSymbolCounts[job.job_type_symbol] > 1 && duplicateJobsVisible)
) {
// render the job itself, not a count
@ -142,7 +142,7 @@ export class JobGroupComponent extends React.Component {
job={job}
filterModel={filterModel}
visible={job.visible}
resultStatus={getStatus(job)}
resultStatus={job.resultStatus}
failureClassificationId={job.failure_classification_id}
repoName={repoName}
filterPlatformCb={filterPlatformCb}

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

@ -1,8 +1,6 @@
import PropTypes from 'prop-types';
import React from 'react';
import { getStatus } from '../../helpers/job';
import JobButton from './JobButton';
import JobGroup from './JobGroup';
@ -43,7 +41,7 @@ export default class JobsAndGroups extends React.Component {
filterModel={filterModel}
repoName={repoName}
visible={job.visible}
resultStatus={getStatus(job)}
resultStatus={job.resultStatus}
failureClassificationId={job.failure_classification_id}
filterPlatformCb={filterPlatformCb}
key={job.id}

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

@ -129,12 +129,12 @@ class Push extends React.PureComponent {
}
};
toggleSelectedRunnableJob = buildername => {
toggleSelectedRunnableJob = signature => {
const { selectedRunnableJobs } = this.state;
const jobIndex = selectedRunnableJobs.indexOf(buildername);
const jobIndex = selectedRunnableJobs.indexOf(signature);
if (jobIndex === -1) {
selectedRunnableJobs.push(buildername);
selectedRunnableJobs.push(signature);
} else {
selectedRunnableJobs.splice(jobIndex, 1);
}

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

@ -89,7 +89,7 @@ class PushJobs extends React.Component {
handleRunnableClick = jobInstance => {
const { toggleSelectedRunnableJob } = this.props;
toggleSelectedRunnableJob(jobInstance.props.job.ref_data_name);
toggleSelectedRunnableJob(jobInstance.props.job.signature);
jobInstance.toggleRunnableSelected();
};

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

@ -62,7 +62,7 @@ class App extends React.PureComponent {
JobModel.get(repoName, jobId)
.then(async job => {
// set the title of the browser window/tab
document.title = job.getTitle();
document.title = job.title;
const rawLogUrl = job.logs && job.logs.length ? job.logs[0].url : null;
// other properties, in order of appearance
// Test to disable successful steps checkbox on taskcluster jobs

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

@ -5,7 +5,7 @@ import {
thFailureResults,
thPlatformMap,
} from '../helpers/constants';
import { getStatus, isClassified } from '../helpers/job';
import { isClassified } from '../helpers/job';
import {
arraysEqual,
matchesDefaults,
@ -196,10 +196,11 @@ export default class FilterModel {
showJob = job => {
// when runnable jobs have been added to a resultset, they should be
// shown regardless of settings for classified or result state
const status = getStatus(job);
if (status !== 'runnable') {
const { resultStatus } = job;
if (resultStatus !== 'runnable') {
// test against resultStatus and classifiedState
if (!this.urlParams.resultStatus.includes(status)) {
if (!this.urlParams.resultStatus.includes(resultStatus)) {
return false;
}
if (!this._checkClassifiedStateFilters(job)) {
@ -273,9 +274,9 @@ export default class FilterModel {
}`;
}
if (field === 'searchStr') {
// lazily get this to avoid storing redundant information
return job.getSearchStr();
if (field === 'resultStatus') {
// don't check this here.
return null;
}
return job[field];
};

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

@ -2,9 +2,9 @@ import { slugid } from 'taskcluster-client-web';
import groupBy from 'lodash/groupBy';
import keyBy from 'lodash/keyBy';
import { thPlatformMap } from '../helpers/constants';
import { createQueryParams, getApiUrl } from '../helpers/url';
import { formatTaskclusterError } from '../helpers/errorMessage';
import { addAggregateFields } from '../helpers/job';
import { getProjectUrl } from '../helpers/location';
import { getData } from '../helpers/http';
@ -15,45 +15,12 @@ const uri = '/jobs/';
// JobModel is the js counterpart of job
export default class JobModel {
constructor(props) {
Object.assign(this, props);
}
getTitle() {
// we want to join the group and type information together
// so we can search for it as one token (useful when
// we want to do a search on something like `fxup-esr(`)
let symbolInfo = this.job_group_symbol === '?' ? '' : this.job_group_symbol;
symbolInfo += `(${this.job_type_symbol})`;
return [
thPlatformMap[this.platform] || this.platform,
this.platform_option,
this.job_group_name === 'unknown' ? undefined : this.job_group_name,
this.job_type_name,
symbolInfo,
]
.filter(item => typeof item !== 'undefined')
.join(' ');
}
getSearchStr() {
return [
this.getTitle(),
this.ref_data_name,
this.signature !== this.ref_data_name ? this.signature : undefined,
]
.filter(item => typeof item !== 'undefined')
.join(' ');
}
static async getList(options, config = {}) {
// The `uri` config allows to fetch a list of jobs from an arbitrary
// endpoint e.g. the similar jobs endpoint. It defaults to the job
// list endpoint.
const { fetchAll, uri: configUri } = config;
const jobUri = configUri || getProjectUrl(uri);
const { data, failureStatus } = await getData(
`${jobUri}${options ? createQueryParams(options) : ''}`,
);
@ -81,17 +48,16 @@ export default class JobModel {
if (job_property_names) {
// the results came as list of fields
// we need to convert them to objects
itemList = results.map(
elem =>
new JobModel(
job_property_names.reduce(
(prev, prop, i) => ({ ...prev, [prop]: elem[i] }),
{},
),
itemList = results.map(elem =>
addAggregateFields(
job_property_names.reduce(
(prev, prop, i) => ({ ...prev, [prop]: elem[i] }),
{},
),
),
);
} else {
itemList = results.map(job_obj => new JobModel(job_obj));
itemList = results.map(job_obj => addAggregateFields(job_obj));
}
return { data: [...itemList, ...nextPagesJobs], failureStatus: null };
}
@ -104,7 +70,7 @@ export default class JobModel {
async response => {
if (response.ok) {
const job = await response.json();
return new JobModel(job);
return addAggregateFields(job);
}
const text = await response.text();
throw Error(`Loading job with id ${pk} : ${text}`);

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

@ -1,8 +1,7 @@
import { addAggregateFields } from '../helpers/job';
import { getRunnableJobsURL } from '../helpers/url';
import { escapeId } from '../helpers/aggregateId';
import JobModel from './job';
export default class RunnableJobModel {
constructor(data) {
Object.assign(this, data);
@ -12,23 +11,22 @@ export default class RunnableJobModel {
const uri = getRunnableJobsURL(params.decision_task_id);
const rawJobs = await fetch(uri).then(response => response.json());
return Object.entries(rawJobs).map(
([key, value]) =>
new JobModel({
build_platform: value.platform || '',
build_system_type: 'taskcluster',
job_group_name: value.groupName || '',
job_group_symbol: value.groupSymbol || '',
job_type_name: key,
job_type_symbol: value.symbol,
platform: value.platform || '',
platform_option: Object.keys(value.collection).join(' '),
ref_data_name: key,
state: 'runnable',
result: 'runnable',
push_id: params.push_id,
id: escapeId(params.push_id + key),
}),
return Object.entries(rawJobs).map(([key, value]) =>
addAggregateFields({
build_platform: value.platform || '',
build_system_type: 'taskcluster',
job_group_name: value.groupName || '',
job_group_symbol: value.groupSymbol || '',
job_type_name: key,
job_type_symbol: value.symbol,
platform: value.platform || '',
platform_option: Object.keys(value.collection).join(' '),
signature: key,
state: 'runnable',
result: 'runnable',
push_id: params.push_id,
id: escapeId(params.push_id + key),
}),
);
}
}

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

@ -2,23 +2,16 @@ import React from 'react';
import PropTypes from 'prop-types';
import { getInspectTaskUrl } from '../helpers/url';
import { getSearchStr, getJobSearchStrHref } from '../helpers/job';
import { getJobSearchStrHref } from '../helpers/job';
import { toDateStr } from '../helpers/display';
const getTimeFields = function getTimeFields(job) {
// time fields to show in detail panel, but that should be grouped together
const { end_timestamp, start_timestamp, submit_timestamp } = job;
const { end_timestamp, start_timestamp, submit_timestamp, duration } = job;
const timeFields = [
{ title: 'Requested', value: toDateStr(submit_timestamp) },
];
// If start time is 0, then duration should be from requesttime to now
// If we have starttime and no endtime, then duration should be starttime to now
// If we have both starttime and endtime, then duration will be between those two
const endtime = end_timestamp || Date.now() / 1000;
const starttime = start_timestamp || submit_timestamp;
const duration = `${Math.round((endtime - starttime) / 60, 0)} minute(s)`;
if (start_timestamp) {
timeFields.push({ title: 'Started', value: toDateStr(start_timestamp) });
}
@ -36,7 +29,16 @@ const getTimeFields = function getTimeFields(job) {
export default class JobInfo extends React.PureComponent {
render() {
const { job, extraFields, showJobFilters } = this.props;
const jobSearchStr = getSearchStr(job);
const {
searchStr,
signature,
title,
taskcluster_metadata,
build_platform,
job_type_name,
build_architecture,
build_os,
} = job;
const timeFields = getTimeFields(job);
return (
@ -47,44 +49,43 @@ export default class JobInfo extends React.PureComponent {
<React.Fragment>
<a
title="Filter jobs with this unique SHA signature"
href={getJobSearchStrHref(job.signature)}
href={getJobSearchStrHref(signature)}
>
(sig)
</a>
:&nbsp;
<a
title="Filter jobs containing these keywords"
href={getJobSearchStrHref(jobSearchStr)}
href={getJobSearchStrHref(searchStr)}
>
{jobSearchStr}
{searchStr}
</a>
</React.Fragment>
) : (
<span>{job.getTitle()}</span>
<span>{title}</span>
)}
</li>
{job.taskcluster_metadata && (
{taskcluster_metadata && (
<li className="small">
<strong>Task: </strong>
<a
id="taskInfo"
href={getInspectTaskUrl(job.taskcluster_metadata.task_id)}
href={getInspectTaskUrl(taskcluster_metadata.task_id)}
target="_blank"
rel="noopener noreferrer"
>
{job.taskcluster_metadata.task_id}
{taskcluster_metadata.task_id}
</a>
</li>
)}
<li className="small">
<strong>Build: </strong>
<span>{`${job.build_architecture} ${
job.build_platform
} ${job.build_os || ''}`}</span>
<span>{`${build_architecture} ${build_platform} ${build_os ||
''}`}</span>
</li>
<li className="small">
<strong>Job name: </strong>
<span>{job.job_type_name}</span>
<span>{job_type_name}</span>
</li>
{[...timeFields, ...extraFields].map(field => (
<li className="small" key={`${field.title}${field.value}`}>