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

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

@ -12,7 +12,8 @@ import FilterModel from '../../../ui/models/filter';
import { store } from '../../../ui/job-view/redux/store'; import { store } from '../../../ui/job-view/redux/store';
import { PinnedJobs } from '../../../ui/job-view/context/PinnedJobs'; import { PinnedJobs } from '../../../ui/job-view/context/PinnedJobs';
import { getUrlParam, setUrlParam } from '../../../ui/helpers/location'; 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 = { const testPush = {
id: 494796, id: 494796,
@ -34,170 +35,13 @@ const testPush = {
jobsLoaded: true, jobsLoaded: true,
}; };
const testPlatforms = [ beforeAll(() => {
{ platforms.forEach(platform =>
name: 'Linux x64', platform.groups.forEach(group =>
option: 'opt', group.jobs.forEach(job => addAggregateFields(job)),
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,
},
],
},
];
afterEach(() => { afterEach(() => {
cleanup(); cleanup();
@ -209,7 +53,7 @@ const testPushJobs = filterModel => (
<PinnedJobs> <PinnedJobs>
<PushJobs <PushJobs
push={testPush} push={testPush}
platforms={testPlatforms} platforms={platforms}
repoName="try" repoName="try"
filterModel={filterModel} filterModel={filterModel}
pushGroupState="" pushGroupState=""

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

@ -30,12 +30,17 @@ import {
fetchNextPushes, fetchNextPushes,
updateRange, updateRange,
} from '../../../../ui/job-view/redux/stores/pushes'; } from '../../../../ui/job-view/redux/stores/pushes';
import { addAggregateFields } from '../../../../ui/helpers/job';
const mockStore = configureMockStore([thunk]); const mockStore = configureMockStore([thunk]);
describe('Pushes Redux store', () => { describe('Pushes Redux store', () => {
const repoName = 'autoland'; const repoName = 'autoland';
beforeAll(() => {
Object.values(jobMap).forEach(job => addAggregateFields(job));
});
afterEach(() => { afterEach(() => {
cleanup(); cleanup();
fetchMock.reset(); 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 // choices available for the field filters
export const thFieldChoices = { 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_name: { name: 'job name', matchType: thMatchType.substr },
job_type_symbol: { name: 'job symbol', matchType: thMatchType.exactstr }, job_type_symbol: { name: 'job symbol', matchType: thMatchType.exactstr },
job_group_name: { name: 'group name', matchType: thMatchType.substr }, 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 // failure classification ids that should be shown in "unclassified" mode
export const thUnclassifiedIds = [1, 7]; 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. // 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`` // These also apply to result "groupings" like ``failures`` and ``in progress``
// for the colored filter chicklets on the nav bar. // for the colored filter chicklets on the nav bar.
@ -40,18 +32,12 @@ export const getBtnClass = function getBtnClass(
let btnClass = btnClasses[resultStatus] || 'btn-default'; let btnClass = btnClasses[resultStatus] || 'btn-default';
// handle if a job is classified // handle if a job is classified
// TODO: Check if the parseInt() is really needed here. if (failureClassificationId > 1) {
const classificationId = parseInt(failureClassificationId, 10);
if (classificationId > 1) {
btnClass += '-classified'; btnClass += '-classified';
} }
return btnClass; return btnClass;
}; };
export const getJobBtnClass = function getJobBtnClass(job) {
return getBtnClass(getStatus(job), job.failure_classification_id);
};
export const isReftest = function isReftest(job) { export const isReftest = function isReftest(job) {
const { const {
job_group_name: gName, 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 // we want to join the group and type information together
// so we can search for it as one token (useful when // so we can search for it as one token (useful when
// we want to do a search on something like `fxup-esr(`) // 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 [ job.title = [
thPlatformMap[job.platform] || job.platform, thPlatformMap[platform] || platform,
job.platform_option, platform_option,
job.job_group_name === 'unknown' ? undefined : job.job_group_name, job_group_name === 'unknown' ? undefined : job_group_name,
job.job_type_name, job_type_name,
`${symbolInfo}(${job.job_type_symbol})`, `${symbolInfo}(${job_type_symbol})`,
] ]
.filter(item => typeof item !== 'undefined') .filter(item => typeof item !== 'undefined')
.join(' ') .join(' ')
.toLowerCase(); .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) { export const getJobSearchStrHref = function getJobSearchStrHref(jobSearchStr) {
@ -227,9 +245,3 @@ export const getJobSearchStrHref = function getJobSearchStrHref(jobSearchStr) {
return `${uiJobsUrlBase}?${params.toString()}`; 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 { thEvents, thBugSuggestionLimit } from '../../helpers/constants';
import { withPinnedJobs } from '../context/PinnedJobs'; import { withPinnedJobs } from '../context/PinnedJobs';
import { addAggregateFields } from '../../helpers/job';
import { getLogViewerUrl, getReftestUrl } from '../../helpers/url'; import { getLogViewerUrl, getReftestUrl } from '../../helpers/url';
import BugJobMapModel from '../../models/bugJobMap'; import BugJobMapModel from '../../models/bugJobMap';
import BugSuggestionsModel from '../../models/bugSuggestions'; import BugSuggestionsModel from '../../models/bugSuggestions';
@ -29,6 +30,7 @@ class DetailsPanel extends React.Component {
this.selectJobController = null; this.selectJobController = null;
this.state = { this.state = {
selectedJobFull: null,
jobDetails: [], jobDetails: [],
jobLogUrls: [], jobLogUrls: [],
jobDetailLoading: false, jobDetailLoading: false,
@ -182,7 +184,7 @@ class DetailsPanel extends React.Component {
); );
const jobDetailPromise = JobDetailModel.getJobDetails( const jobDetailPromise = JobDetailModel.getJobDetails(
{ job_guid: selectedJob.job_guid }, { job_id: selectedJob.id },
this.selectJobController.signal, 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 // 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 // is what we'll pass to the rest of the details panel. It has extra fields like
// taskcluster_metadata. // taskcluster_metadata.
Object.assign(selectedJob, jobResult); const selectedJobFull = jobResult;
const jobRevision = push.revision; const jobRevision = push.revision;
jobDetails = jobDetailResult; addAggregateFields(selectedJobFull);
// incorporate the buildername into the job details if this is a buildbot job jobDetails = jobDetailResult;
// (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 },
];
}
// the third result comes from the jobLogUrl promise // the third result comes from the jobLogUrl promise
// exclude the json log URLs // exclude the json log URLs
@ -292,6 +284,7 @@ class DetailsPanel extends React.Component {
this.setState( this.setState(
{ {
selectedJobFull,
jobLogUrls, jobLogUrls,
jobDetails, jobDetails,
logParseStatus, logParseStatus,
@ -325,9 +318,9 @@ class DetailsPanel extends React.Component {
classificationMap, classificationMap,
classificationTypes, classificationTypes,
isPinBoardVisible, isPinBoardVisible,
selectedJob,
} = this.props; } = this.props;
const { const {
selectedJobFull,
jobDetails, jobDetails,
jobRevision, jobRevision,
jobLogUrls, jobLogUrls,
@ -351,19 +344,19 @@ class DetailsPanel extends React.Component {
<div <div
id="details-panel" id="details-panel"
style={{ height: `${detailsPanelHeight}px` }} style={{ height: `${detailsPanelHeight}px` }}
className={selectedJob ? 'details-panel-slide' : 'hidden'} className={selectedJobFull ? 'details-panel-slide' : 'hidden'}
> >
<PinBoard <PinBoard
repoName={repoName} repoName={repoName}
currentRepo={currentRepo} currentRepo={currentRepo}
isLoggedIn={user.isLoggedIn || false} isLoggedIn={user.isLoggedIn || false}
classificationTypes={classificationTypes} classificationTypes={classificationTypes}
selectedJob={selectedJob} selectedJobFull={selectedJobFull}
/> />
{!!selectedJob && ( {!!selectedJobFull && (
<div id="details-panel-content"> <div id="details-panel-content">
<SummaryPanel <SummaryPanel
selectedJob={selectedJob} selectedJobFull={selectedJobFull}
repoName={repoName} repoName={repoName}
currentRepo={currentRepo} currentRepo={currentRepo}
classificationMap={classificationMap} classificationMap={classificationMap}
@ -380,7 +373,7 @@ class DetailsPanel extends React.Component {
/> />
<span className="job-tabs-divider" /> <span className="job-tabs-divider" />
<TabsPanel <TabsPanel
selectedJob={selectedJob} selectedJobFull={selectedJobFull}
jobDetails={jobDetails} jobDetails={jobDetails}
perfJobDetail={perfJobDetail} perfJobDetail={perfJobDetail}
repoName={repoName} repoName={repoName}

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

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

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

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

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

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

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

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

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

@ -175,9 +175,13 @@ class AnnotationsTab extends React.Component {
}; };
deleteClassification = classification => { 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(); recalculateUnclassifiedCounts();
classification.destroy().then( classification.destroy().then(
@ -247,16 +251,10 @@ AnnotationsTab.propTypes = {
classifications: PropTypes.array.isRequired, classifications: PropTypes.array.isRequired,
recalculateUnclassifiedCounts: PropTypes.func.isRequired, recalculateUnclassifiedCounts: PropTypes.func.isRequired,
notify: 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( export default connect(
mapStateToProps, null,
{ notify, recalculateUnclassifiedCounts }, { notify, recalculateUnclassifiedCounts },
)(AnnotationsTab); )(AnnotationsTab);

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

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

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

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

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

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

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

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

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

@ -22,7 +22,7 @@ export default class SuggestionsListItem extends React.Component {
}; };
render() { render() {
const { suggestion, toggleBugFiler } = this.props; const { suggestion, toggleBugFiler, selectedJobFull } = this.props;
const { suggestionShowMore } = this.state; const { suggestionShowMore } = this.state;
return ( return (
@ -42,7 +42,12 @@ export default class SuggestionsListItem extends React.Component {
{suggestion.valid_open_recent && ( {suggestion.valid_open_recent && (
<ul className="list-unstyled failure-summary-bugs"> <ul className="list-unstyled failure-summary-bugs">
{suggestion.bugs.open_recent.map(bug => ( {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> </ul>
)} )}
@ -68,6 +73,7 @@ export default class SuggestionsListItem extends React.Component {
suggestion={suggestion} suggestion={suggestion}
bugClassName={bug.resolution !== '' ? 'strike-through' : ''} bugClassName={bug.resolution !== '' ? 'strike-through' : ''}
title={bug.resolution !== '' ? bug.resolution : ''} title={bug.resolution !== '' ? bug.resolution : ''}
selectedJobFull={selectedJobFull}
/> />
))} ))}
</ul> </ul>
@ -87,6 +93,7 @@ export default class SuggestionsListItem extends React.Component {
} }
SuggestionsListItem.propTypes = { SuggestionsListItem.propTypes = {
selectedJobFull: PropTypes.object.isRequired,
suggestion: PropTypes.object.isRequired, suggestion: PropTypes.object.isRequired,
toggleBugFiler: PropTypes.func.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 faStarRegular } from '@fortawesome/free-regular-svg-icons';
import { faStar as faStarSolid } from '@fortawesome/free-solid-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'; import { getSelectedJobId, getUrlParam } from '../../helpers/location';
export default class JobButtonComponent extends React.Component { export default class JobButtonComponent extends React.Component {
@ -80,24 +80,19 @@ export default class JobButtonComponent extends React.Component {
} }
render() { render() {
const { job, resultStatus } = this.props; const { job } = this.props;
const { isSelected, isRunnableSelected } = this.state; const { isSelected, isRunnableSelected } = this.state;
const { const {
state, state,
job_type_name,
failure_classification_id, failure_classification_id,
end_timestamp,
start_timestamp,
ref_data_name,
visible, visible,
id, id,
job_type_symbol, job_type_symbol,
btnClass,
} = job; } = job;
if (!visible) return null; if (!visible) return null;
const runnable = state === 'runnable'; const runnable = state === 'runnable';
const btnClass = getBtnClass(resultStatus, failure_classification_id);
let title = `${resultStatus} | ${job_type_name}`;
let classifiedIcon = null; let classifiedIcon = null;
if (failure_classification_id > 1) { if (failure_classification_id > 1) {
@ -105,20 +100,14 @@ export default class JobButtonComponent extends React.Component {
failure_classification_id === 7 ? faStarRegular : faStarSolid; 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 classes = ['btn', btnClass, 'filter-shown'];
const attributes = { const attributes = {
'data-job-id': id, 'data-job-id': id,
title, title: job.hoverText,
}; };
if (runnable) { if (runnable) {
classes.push('runnable-job-btn', 'runnable'); classes.push('runnable-job-btn', 'runnable');
attributes['data-buildername'] = ref_data_name;
if (isRunnableSelected) { if (isRunnableSelected) {
classes.push('runnable-job-btn-selected'); classes.push('runnable-job-btn-selected');
} }

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -2,9 +2,9 @@ import { slugid } from 'taskcluster-client-web';
import groupBy from 'lodash/groupBy'; import groupBy from 'lodash/groupBy';
import keyBy from 'lodash/keyBy'; import keyBy from 'lodash/keyBy';
import { thPlatformMap } from '../helpers/constants';
import { createQueryParams, getApiUrl } from '../helpers/url'; import { createQueryParams, getApiUrl } from '../helpers/url';
import { formatTaskclusterError } from '../helpers/errorMessage'; import { formatTaskclusterError } from '../helpers/errorMessage';
import { addAggregateFields } from '../helpers/job';
import { getProjectUrl } from '../helpers/location'; import { getProjectUrl } from '../helpers/location';
import { getData } from '../helpers/http'; import { getData } from '../helpers/http';
@ -15,45 +15,12 @@ const uri = '/jobs/';
// JobModel is the js counterpart of job // JobModel is the js counterpart of job
export default class JobModel { 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 = {}) { static async getList(options, config = {}) {
// The `uri` config allows to fetch a list of jobs from an arbitrary // 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 // endpoint e.g. the similar jobs endpoint. It defaults to the job
// list endpoint. // list endpoint.
const { fetchAll, uri: configUri } = config; const { fetchAll, uri: configUri } = config;
const jobUri = configUri || getProjectUrl(uri); const jobUri = configUri || getProjectUrl(uri);
const { data, failureStatus } = await getData( const { data, failureStatus } = await getData(
`${jobUri}${options ? createQueryParams(options) : ''}`, `${jobUri}${options ? createQueryParams(options) : ''}`,
); );
@ -81,17 +48,16 @@ export default class JobModel {
if (job_property_names) { if (job_property_names) {
// the results came as list of fields // the results came as list of fields
// we need to convert them to objects // we need to convert them to objects
itemList = results.map( itemList = results.map(elem =>
elem => addAggregateFields(
new JobModel( job_property_names.reduce(
job_property_names.reduce( (prev, prop, i) => ({ ...prev, [prop]: elem[i] }),
(prev, prop, i) => ({ ...prev, [prop]: elem[i] }), {},
{},
),
), ),
),
); );
} else { } else {
itemList = results.map(job_obj => new JobModel(job_obj)); itemList = results.map(job_obj => addAggregateFields(job_obj));
} }
return { data: [...itemList, ...nextPagesJobs], failureStatus: null }; return { data: [...itemList, ...nextPagesJobs], failureStatus: null };
} }
@ -104,7 +70,7 @@ export default class JobModel {
async response => { async response => {
if (response.ok) { if (response.ok) {
const job = await response.json(); const job = await response.json();
return new JobModel(job); return addAggregateFields(job);
} }
const text = await response.text(); const text = await response.text();
throw Error(`Loading job with id ${pk} : ${text}`); throw Error(`Loading job with id ${pk} : ${text}`);

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

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

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

@ -2,23 +2,16 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { getInspectTaskUrl } from '../helpers/url'; import { getInspectTaskUrl } from '../helpers/url';
import { getSearchStr, getJobSearchStrHref } from '../helpers/job'; import { getJobSearchStrHref } from '../helpers/job';
import { toDateStr } from '../helpers/display'; import { toDateStr } from '../helpers/display';
const getTimeFields = function getTimeFields(job) { const getTimeFields = function getTimeFields(job) {
// time fields to show in detail panel, but that should be grouped together // 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 = [ const timeFields = [
{ title: 'Requested', value: toDateStr(submit_timestamp) }, { 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) { if (start_timestamp) {
timeFields.push({ title: 'Started', value: toDateStr(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 { export default class JobInfo extends React.PureComponent {
render() { render() {
const { job, extraFields, showJobFilters } = this.props; 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); const timeFields = getTimeFields(job);
return ( return (
@ -47,44 +49,43 @@ export default class JobInfo extends React.PureComponent {
<React.Fragment> <React.Fragment>
<a <a
title="Filter jobs with this unique SHA signature" title="Filter jobs with this unique SHA signature"
href={getJobSearchStrHref(job.signature)} href={getJobSearchStrHref(signature)}
> >
(sig) (sig)
</a> </a>
:&nbsp; :&nbsp;
<a <a
title="Filter jobs containing these keywords" title="Filter jobs containing these keywords"
href={getJobSearchStrHref(jobSearchStr)} href={getJobSearchStrHref(searchStr)}
> >
{jobSearchStr} {searchStr}
</a> </a>
</React.Fragment> </React.Fragment>
) : ( ) : (
<span>{job.getTitle()}</span> <span>{title}</span>
)} )}
</li> </li>
{job.taskcluster_metadata && ( {taskcluster_metadata && (
<li className="small"> <li className="small">
<strong>Task: </strong> <strong>Task: </strong>
<a <a
id="taskInfo" id="taskInfo"
href={getInspectTaskUrl(job.taskcluster_metadata.task_id)} href={getInspectTaskUrl(taskcluster_metadata.task_id)}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
{job.taskcluster_metadata.task_id} {taskcluster_metadata.task_id}
</a> </a>
</li> </li>
)} )}
<li className="small"> <li className="small">
<strong>Build: </strong> <strong>Build: </strong>
<span>{`${job.build_architecture} ${ <span>{`${build_architecture} ${build_platform} ${build_os ||
job.build_platform ''}`}</span>
} ${job.build_os || ''}`}</span>
</li> </li>
<li className="small"> <li className="small">
<strong>Job name: </strong> <strong>Job name: </strong>
<span>{job.job_type_name}</span> <span>{job_type_name}</span>
</li> </li>
{[...timeFields, ...extraFields].map(field => ( {[...timeFields, ...extraFields].map(field => (
<li className="small" key={`${field.title}${field.value}`}> <li className="small" key={`${field.title}${field.value}`}>