ui/job-view/details/summary/ActionBar.jsx (464 lines of code) (raw):
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import {
Button,
DropdownMenu,
DropdownItem,
DropdownToggle,
UncontrolledDropdown,
} from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faChartBar } from '@fortawesome/free-regular-svg-icons';
import {
faEllipsisH,
faRedo,
faThumbtack,
faTimesCircle,
faCrosshairs,
} from '@fortawesome/free-solid-svg-icons';
import {
geckoProfileTaskName,
sxsTaskName,
thEvents,
} from '../../../helpers/constants';
import { triggerTask } from '../../../helpers/performance';
import { formatTaskclusterError } from '../../../helpers/errorMessage';
import {
isReftest,
isPerfTest,
canConfirmFailure,
confirmFailure,
findJobInstance,
} from '../../../helpers/job';
import { getInspectTaskUrl, getReftestUrl } from '../../../helpers/url';
import JobModel from '../../../models/job';
import TaskclusterModel from '../../../models/taskcluster';
import CustomJobActions from '../../CustomJobActions';
import { notify } from '../../redux/stores/notifications';
import { pinJob } from '../../redux/stores/pinnedJobs';
import { getAction } from '../../../helpers/taskcluster';
import { checkRootUrl } from '../../../taskcluster-auth-callback/constants';
import LogUrls from './LogUrls';
class ActionBar extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
customJobActionsShowing: false,
};
}
componentDidMount() {
window.addEventListener(thEvents.openLogviewer, this.onOpenLogviewer);
window.addEventListener(thEvents.jobRetrigger, this.onRetriggerJob);
}
componentWillUnmount() {
window.removeEventListener(thEvents.openLogviewer, this.onOpenLogviewer);
window.removeEventListener(thEvents.jobRetrigger, this.onRetriggerJob);
}
onRetriggerJob = (event) => {
this.retriggerJob([event.detail.job]);
};
// Open the logviewer and provide notifications if it isn't available
onOpenLogviewer = () => {
const { logParseStatus, notify } = this.props;
switch (logParseStatus) {
case 'pending':
notify('Log parsing in progress, log viewer not yet available');
break;
case 'failed':
notify('Log parsing has failed, log viewer is unavailable', 'warning');
break;
case 'skipped-size':
notify('Log parsing was skipped, log viewer is unavailable', 'warning');
break;
case 'unavailable':
notify('No logs available for this job');
break;
case 'parsed':
document.querySelector('.logviewer-btn').click();
}
};
canCancel = () => {
const { selectedJobFull } = this.props;
return (
selectedJobFull.state === 'pending' || selectedJobFull.state === 'running'
);
};
createGeckoProfile = async () => {
const {
selectedJobFull,
notify,
decisionTaskMap,
currentRepo,
} = this.props;
return triggerTask(
selectedJobFull,
notify,
decisionTaskMap,
currentRepo,
geckoProfileTaskName,
);
};
createSideBySide = async () => {
const {
selectedJobFull,
notify,
decisionTaskMap,
currentRepo,
} = this.props;
await triggerTask(
selectedJobFull,
notify,
decisionTaskMap,
currentRepo,
sxsTaskName,
);
};
retriggerJob = async (jobs) => {
const { notify, decisionTaskMap, currentRepo } = this.props;
// Spin the retrigger button when retriggers happen
document
.querySelector('#retrigger-btn > svg')
.classList.remove('action-bar-spin');
window.requestAnimationFrame(() => {
window.requestAnimationFrame(() => {
document
.querySelector('#retrigger-btn > svg')
.classList.add('action-bar-spin');
});
});
JobModel.retrigger(jobs, currentRepo, notify, 1, decisionTaskMap);
};
backfillJob = async () => {
const {
selectedJobFull,
notify,
decisionTaskMap,
currentRepo,
} = this.props;
if (!this.canBackfill()) {
return;
}
if (!selectedJobFull.id) {
notify('Job not yet loaded for backfill', 'warning');
return;
}
const { id: decisionTaskId } = decisionTaskMap[selectedJobFull.push_id];
TaskclusterModel.load(decisionTaskId, selectedJobFull, currentRepo).then(
(results) => {
try {
const backfilltask = getAction(results.actions, 'backfill');
return TaskclusterModel.submit({
action: backfilltask,
decisionTaskId,
taskId: results.originalTaskId,
input: {},
staticActionVariables: results.staticActionVariables,
currentRepo,
}).then(
() => {
notify(
'Request sent to backfill job via actions.json',
'success',
);
},
(e) => {
// The full message is too large to fit in a Treeherder
// notification box.
notify(formatTaskclusterError(e), 'danger', { sticky: true });
},
);
} catch (e) {
notify(formatTaskclusterError(e), 'danger', { sticky: true });
}
},
);
};
handleConfirmFailure = async () => {
const {
selectedJobFull,
notify,
decisionTaskMap,
currentRepo,
} = this.props;
confirmFailure(selectedJobFull, notify, decisionTaskMap, currentRepo);
};
// Can we backfill? At the moment, this only ensures we're not in a 'try' repo.
canBackfill = () => {
const { isTryRepo } = this.props;
return !isTryRepo;
};
backfillButtonTitle = () => {
const { isTryRepo } = this.props;
let title = '';
if (isTryRepo) {
title = title.concat('backfill not available in this repository');
}
if (title === '') {
title =
'Trigger jobs of this type on prior pushes ' +
'to fill in gaps where the job was not run';
} else {
// Cut off trailing '/ ' if one exists, capitalize first letter
title = title.replace(/\/ $/, '');
title = title.replace(/^./, (l) => l.toUpperCase());
}
return title;
};
createInteractiveTask = async () => {
const {
user,
selectedJobFull,
notify,
decisionTaskMap,
currentRepo,
} = this.props;
const { id: decisionTaskId } = decisionTaskMap[selectedJobFull.push_id];
const results = await TaskclusterModel.load(
decisionTaskId,
selectedJobFull,
currentRepo,
);
try {
const interactiveTask = getAction(results.actions, 'create-interactive');
if (user.email === '') {
notify('Please login before creating an interactive task');
return;
}
await TaskclusterModel.submit({
action: interactiveTask,
decisionTaskId,
taskId: results.originalTaskId,
input: {
notify: user.email,
},
staticActionVariables: results.staticActionVariables,
currentRepo,
});
notify(
`Request sent to create an interactive job via actions.json.
You will soon receive an email containing a link to interact with the task.`,
'success',
);
} catch (e) {
// The full message is too large to fit in a Treeherder
// notification box.
notify(formatTaskclusterError(e), 'danger', { sticky: true });
}
};
cancelJobs = (jobs) => {
const { notify, decisionTaskMap, currentRepo } = this.props;
JobModel.cancel(
jobs.filter(({ state }) => state === 'pending' || state === 'running'),
currentRepo,
notify,
decisionTaskMap,
);
};
cancelJob = () => {
this.cancelJobs([this.props.selectedJobFull]);
};
toggleCustomJobActions = () => {
const { customJobActionsShowing } = this.state;
this.setState({ customJobActionsShowing: !customJobActionsShowing });
};
render() {
const {
selectedJobFull,
logViewerUrl,
logViewerFullUrl,
jobLogUrls,
pinJob,
currentRepo,
} = this.props;
const { customJobActionsShowing } = this.state;
return (
<div id="actionbar">
<nav className="navbar navbar-dark details-panel-navbar">
<ul className="nav actionbar-nav">
<LogUrls
logUrls={jobLogUrls}
logViewerUrl={logViewerUrl}
logViewerFullUrl={logViewerFullUrl}
/>
<li>
<Button
id="pin-job-btn"
title="Add this job to the pinboard"
className="actionbar-nav-btn icon-blue bg-transparent border-0"
onClick={() => pinJob(selectedJobFull)}
>
<FontAwesomeIcon icon={faThumbtack} title="Pin job" />
</Button>
</li>
<li>
<Button
id="retrigger-btn"
title="Repeat the selected job"
className="actionbar-nav-btn bg-transparent border-0 icon-green"
onClick={() => this.retriggerJob([selectedJobFull])}
>
<FontAwesomeIcon icon={faRedo} title="Retrigger job" />
</Button>
</li>
{isReftest(selectedJobFull) &&
jobLogUrls.map((jobLogUrl) => (
<li key={`reftest-${jobLogUrl.id}`}>
<a
title="Launch the Reftest Analyzer in a new window"
className="actionbar-nav-btn"
target="_blank"
rel="noopener noreferrer"
href={getReftestUrl(jobLogUrl.url)}
>
<FontAwesomeIcon
icon={faChartBar}
title="Reftest analyzer"
/>
</a>
</li>
))}
<li>
<Button
id="find-job-btn"
title="Scroll to selection"
className="actionbar-nav-btn icon-blue bg-transparent border-0"
onClick={() =>
findJobInstance(jobLogUrls[0] && jobLogUrls[0].job_id, true)
}
>
<FontAwesomeIcon
icon={faCrosshairs}
title="Find job instance"
/>
</Button>
</li>
{this.canCancel() && (
<li>
<Button
title="Must be logged in to cancel a job"
className="bg-transparent border-0 actionbar-nav-btn hover-warning"
onClick={() => this.cancelJob()}
>
<FontAwesomeIcon icon={faTimesCircle} title="Cancel job" />
</Button>
</li>
)}
<li className="ml-auto">
<UncontrolledDropdown>
<DropdownToggle className="bg-transparent text-light border-0 pr-2 py-2 m-0">
<FontAwesomeIcon
icon={faEllipsisH}
title="Other job actions"
className="align-baseline"
/>
</DropdownToggle>
<DropdownMenu className="actionbar-menu dropdown-menu-right">
<DropdownItem
tag="a"
id="backfill-btn"
className={`${!this.canBackfill() ? 'disabled' : ''}`}
title={this.backfillButtonTitle()}
onClick={() => !this.canBackfill() || this.backfillJob()}
>
Backfill
</DropdownItem>
{selectedJobFull.task_id && (
<React.Fragment>
<DropdownItem
tag="a"
target="_blank"
rel="noopener noreferrer"
className="pl-4"
href={getInspectTaskUrl(
selectedJobFull.task_id,
checkRootUrl(currentRepo.tc_root_url),
selectedJobFull.submit_timestamp,
)}
>
Inspect Task
</DropdownItem>
<DropdownItem
tag="a"
className="py-2"
onClick={this.createInteractiveTask}
>
Create Interactive Task
</DropdownItem>
{isPerfTest(selectedJobFull) && (
<DropdownItem
tag="a"
className="py-2"
onClick={this.createGeckoProfile}
>
Create Gecko Profile
</DropdownItem>
)}
{isPerfTest(selectedJobFull) &&
!selectedJobFull.hasSideBySide && (
<DropdownItem
tag="a"
className="py-2"
onClick={this.createSideBySide}
>
Generate side-by-side
</DropdownItem>
)}
{canConfirmFailure(selectedJobFull) && (
<DropdownItem
tag="a"
className="py-2"
onClick={this.handleConfirmFailure}
>
Confirm Test Failures
</DropdownItem>
)}
<DropdownItem
tag="a"
onClick={this.toggleCustomJobActions}
className="dropdown-item"
>
Custom Action...
</DropdownItem>
</React.Fragment>
)}
</DropdownMenu>
</UncontrolledDropdown>
</li>
</ul>
</nav>
{customJobActionsShowing && (
<CustomJobActions
job={selectedJobFull}
pushId={selectedJobFull.push_id}
currentRepo={currentRepo}
toggle={this.toggleCustomJobActions}
/>
)}
</div>
);
}
}
ActionBar.propTypes = {
pinJob: PropTypes.func.isRequired,
decisionTaskMap: PropTypes.shape({}).isRequired,
user: PropTypes.shape({}).isRequired,
selectedJobFull: PropTypes.shape({}).isRequired,
logParseStatus: PropTypes.string.isRequired,
notify: PropTypes.func.isRequired,
jobLogUrls: PropTypes.arrayOf(PropTypes.shape({})),
currentRepo: PropTypes.shape({}).isRequired,
isTryRepo: PropTypes.bool,
logViewerUrl: PropTypes.string,
logViewerFullUrl: PropTypes.string,
};
ActionBar.defaultProps = {
isTryRepo: true, // default to more restrictive for backfilling
logViewerUrl: null,
logViewerFullUrl: null,
jobLogUrls: [],
};
const mapStateToProps = ({ pushes: { decisionTaskMap } }) => ({
decisionTaskMap,
});
export default connect(mapStateToProps, { notify, pinJob })(ActionBar);