- {this.$rootScope.currentRepo &&
-
}
+
);
}
}
Push.propTypes = {
push: PropTypes.object.isRequired,
+ isTryRepo: PropTypes.bool,
+ loggedIn: PropTypes.bool,
+ isStaff: PropTypes.bool,
+ repoName: PropTypes.string,
$injector: PropTypes.object.isRequired,
};
-
-treeherder.directive('push', ['reactDirective', '$injector', (reactDirective, $injector) =>
- reactDirective(Push, ['push'], {}, { $injector, store })]);
-
diff --git a/ui/job-view/PushActionMenu.jsx b/ui/job-view/PushActionMenu.jsx
new file mode 100644
index 000000000..b25988af8
--- /dev/null
+++ b/ui/job-view/PushActionMenu.jsx
@@ -0,0 +1,171 @@
+import React from "react";
+import PropTypes from 'prop-types';
+
+export default class PushActionMenu extends React.PureComponent {
+
+ constructor(props) {
+ super(props);
+ const { $injector } = this.props;
+
+ this.$rootScope = $injector.get('$rootScope');
+ this.thEvents = $injector.get('thEvents');
+ this.thNotify = $injector.get('thNotify');
+ this.thJobFilters = $injector.get('thJobFilters');
+ this.ThResultSetStore = $injector.get('ThResultSetStore');
+ this.ThModelErrors = $injector.get('ThModelErrors');
+ this.ThTaskclusterErrors = $injector.get('ThTaskclusterErrors');
+
+ // customPushActions uses $uibModal which doesn't work well in the
+ // unit tests. So if we fail to inject here, that's OK.
+ // Bug 1437736
+ try {
+ this.customPushActions = $injector.get('customPushActions');
+ } catch (ex) {
+ this.customPushActions = {};
+ }
+
+ this.revision = this.props.revision;
+ this.pushId = this.props.pushId;
+ this.repoName = this.props.repoName;
+
+ this.triggerMissingJobs = this.triggerMissingJobs.bind(this);
+ this.triggerAllTalosJobs = this.triggerAllTalosJobs.bind(this);
+
+ // Trigger missing jobs is dangerous on repos other than these (see bug 1335506)
+ this.triggerMissingRepos = ['mozilla-inbound', 'autoland'];
+ }
+
+ triggerMissingJobs() {
+ if (!window.confirm(`This will trigger all missing jobs for revision ${this.revision}!\n\nClick "OK" if you want to proceed.`)) {
+ return;
+ }
+
+ this.ThResultSetStore.getGeckoDecisionTaskId(this.repoName, this.pushId)
+ .then((decisionTaskID) => {
+ this.ThResultSetModel.triggerMissingJobs(decisionTaskID)
+ .then((msg) => {
+ this.thNotify.send(msg, "success");
+ }, (e) => {
+ this.thNotify.send(
+ this.ThModelErrors.format(e, "The action 'trigger missing jobs' failed"),
+ 'danger', true
+ );
+ });
+ });
+ }
+
+ triggerAllTalosJobs() {
+ if (!window.confirm(`This will trigger all Talos jobs for revision ${this.revision}!\n\nClick "OK" if you want to proceed.`)) {
+ return;
+ }
+
+ let times = parseInt(window.prompt("Enter number of instances to have for each talos job", 6));
+ while (times < 1 || times > 6 || isNaN(times)) {
+ times = window.prompt("We only allow instances of each talos job to be between 1 to 6 times. Enter again", 6);
+ }
+
+ this.ThResultSetStore.getGeckoDecisionTaskId(this.repoName, this.pushId)
+ .then((decisionTaskID) => {
+ this.ThResultSetModel.triggerAllTalosJobs(times, decisionTaskID)
+ .then((msg) => {
+ this.thNotify.send(msg, "success");
+ }, (e) => {
+ this.thNotify.send(
+ this.ThTaskclusterErrors.format(e),
+ 'danger',
+ { sticky: true }
+ );
+ });
+ });
+ }
+
+
+ render() {
+ const { loggedIn, isStaff, repoName, revision, pushId, runnableVisible,
+ hideRunnableJobsCb, showRunnableJobsCb } = this.props;
+ const { addFilter } = this.thJobFilters;
+
+ return (
+
+
+
+
+ {runnableVisible ?
+ - hideRunnableJobsCb()}
+ >Hide Runnable Jobs
:
+ - showRunnableJobsCb()}
+ >Add new jobs
+ }
+ - BuildAPI
+ {isStaff && this.triggerMissingRepos.includes(repoName) &&
+ - this.triggerMissingJobs(revision)}
+ >Trigger missing jobs
+ }
+ {isStaff &&
+ - this.triggerAllTalosJobs(revision)}
+ >Trigger all Talos jobs
+ }
+ -
+ Mark with Bugherder
+ - this.customPushActions.open(repoName, pushId)}
+ title="View/Edit/Submit Action tasks for this push"
+ >Custom Push Action...
+ - addFilter('tochange', revision)}
+ >Set as top of range
+ - addFilter('fromchange', revision)}
+ >Set as bottom of range
+
+
+ );
+ }
+}
+
+PushActionMenu.propTypes = {
+ runnableVisible: PropTypes.bool.isRequired,
+ isStaff: PropTypes.bool.isRequired,
+ loggedIn: PropTypes.bool.isRequired,
+ revision: PropTypes.string.isRequired,
+};
diff --git a/ui/job-view/PushHeader.jsx b/ui/job-view/PushHeader.jsx
new file mode 100644
index 000000000..4fcc42b85
--- /dev/null
+++ b/ui/job-view/PushHeader.jsx
@@ -0,0 +1,268 @@
+import React from "react";
+import PropTypes from 'prop-types';
+import { Alert } from 'reactstrap';
+import PushActionMenu from './PushActionMenu';
+
+const Author = (props) => {
+ const authorMatch = props.author.match(/\<(.*?)\>+/);
+ const authorEmail = authorMatch ? authorMatch[1] : props.author;
+
+ return (
+
+ {authorEmail}
+
+ );
+};
+
+const PushCounts = (props) => {
+ const { pending, running, completed } = props;
+ const inProgress = pending + running;
+ const total = completed + inProgress;
+ const percentComplete = total > 0 ?
+ Math.floor(((completed / total) * 100)) : undefined;
+
+ return (
+
+ {percentComplete === 100 ?
+ - Complete - :
+ {percentComplete}% - {inProgress} in progress
+ }
+
+ );
+};
+
+export default class PushHeader extends React.PureComponent {
+
+ constructor(props) {
+ super(props);
+ const { $injector, pushTimestamp, urlBasePath, repoName, revision, author } = this.props;
+
+ this.$rootScope = $injector.get('$rootScope');
+ this.thEvents = $injector.get('thEvents');
+ this.thJobFilters = $injector.get('thJobFilters');
+ this.thNotify = $injector.get('thNotify');
+ this.thPinboard = $injector.get('thPinboard');
+ this.thPinboardCountError = $injector.get('thPinboardCountError');
+ this.thBuildApi = $injector.get('thBuildApi');
+ this.ThResultSetStore = $injector.get('ThResultSetStore');
+ this.ThResultSetModel = $injector.get('ThResultSetModel');
+ this.ThModelErrors = $injector.get('ThModelErrors');
+ this.ThTaskclusterErrors = $injector.get('ThTaskclusterErrors');
+
+ const dateFormat = { weekday: 'short', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric', hour12: false };
+ this.pushDateStr = new Date(pushTimestamp*1000).toLocaleString("en-US", dateFormat);
+ this.revisionPushFilterUrl = `${urlBasePath}?repo=${repoName}&revision=${revision}`;
+ this.authorPushFilterUrl = `${urlBasePath}?repo=${repoName}&author=${encodeURIComponent(author)}`;
+
+ this.pinAllShownJobs = this.pinAllShownJobs.bind(this);
+ this.triggerNewJobs = this.triggerNewJobs.bind(this);
+ this.cancelAllJobs = this.cancelAllJobs.bind(this);
+
+ this.state = {
+ showConfirmCancelAll: false,
+ runnableJobsSelected: false,
+ };
+ }
+
+ componentWillMount() {
+ this.toggleRunnableJobUnlisten = this.$rootScope.$on(
+ this.thEvents.selectRunnableJob, (ev, runnableJobs, pushId) => {
+ if (this.props.pushId === pushId) {
+ this.setState({ runnableJobsSelected: runnableJobs.length > 0 });
+ }
+ }
+ );
+ }
+
+ componentWillUnmount() {
+ this.toggleRunnableJobUnlisten();
+ }
+
+ filterParams() {
+ return Object.entries(this.thJobFilters.getActiveFilters())
+ .reduce((acc, [key, value]) => `&${key}=${value}`, "");
+ }
+
+ triggerNewJobs() {
+ const { repoName, loggedIn, pushId } = this.props;
+
+ if (!window.confirm(
+ 'This will trigger all selected jobs. Click "OK" if you want to proceed.')) {
+ return;
+ }
+ if (loggedIn) {
+ const builderNames = this.ThResultSetStore.getSelectedRunnableJobs(repoName, pushId);
+ this.ThResultSetStore.getGeckoDecisionTaskId(repoName, pushId).then((decisionTaskID) => {
+ this.ThResultSetModel.triggerNewJobs(builderNames, decisionTaskID).then((result) => {
+ this.thNotify.send(result, "success");
+ this.ThResultSetStore.deleteRunnableJobs(repoName, pushId);
+ this.props.hideRunnableJobsCb();
+ this.setState({ runnableJobsSelected: false });
+ }, (e) => {
+ this.thNotify.send(this.ThTaskclusterErrors.format(e), 'danger', { sticky: true });
+ });
+ });
+ } else {
+ this.thNotify.send("Must be logged in to trigger a job", 'danger');
+ }
+ }
+
+ cancelAllJobs() {
+ const { repoName, revision, isTryRepo, isStaff, pushId } = this.props;
+
+ this.setState({ showConfirmCancelAll: false });
+ if (!(isTryRepo || isStaff)) return;
+
+ this.ThResultSetModel.cancelAll(pushId, repoName).then(() => (
+ this.thBuildApi.cancelAll(repoName, revision)
+ )).catch((e) => {
+ this.thNotify.send(
+ this.ThModelErrors.format(e, "Failed to cancel all jobs"),
+ 'danger', true
+ );
+ });
+ }
+
+ pinAllShownJobs() {
+ if (!this.thPinboard.spaceRemaining()) {
+ this.thNotify.send(this.thPinboardCountError, 'danger');
+ return;
+ }
+ const shownJobs = this.ThResultSetStore.getAllShownJobs(
+ this.props.repoName,
+ this.thPinboard.spaceRemaining(),
+ this.thPinboardCountError,
+ this.props.pushId
+ );
+ this.thPinboard.pinJobs(shownJobs);
+
+ if (!this.$rootScope.selectedJob) {
+ this.$rootScope.$emit(this.thEvents.jobClick, shownJobs[0]);
+ }
+ }
+
+ render() {
+ const { repoName, loggedIn, pushId, isTryRepo, isStaff, jobCounts, author,
+ revision, runnableVisible, $injector,
+ showRunnableJobsCb, hideRunnableJobsCb } = this.props;
+
+ const cancelJobsTitle = loggedIn ?
+ "Cancel all jobs" :
+ "Must be logged in to cancel jobs";
+ const canCancelJobs = isTryRepo || isStaff;
+ const counts = jobCounts || { pending: 0, running: 0, completed: 0 };
+
+ return (
+
+
+
+
+
+ {this.pushDateStr}
+ -
+
+
+
+
+
+ View Tests
+ {canCancelJobs &&
+
+ }
+
+ {this.state.runnableJobsSelected && runnableVisible &&
+
+ }
+
+
+
+ {this.state.showConfirmCancelAll &&
+
+
this.setState({ showConfirmCancelAll: false })}>
+
+ This action will cancel all pending and running jobs for this push. It cannot be undone!
+
+
+
+
+ }
+
+ );
+ }
+}
+
+PushHeader.propTypes = {
+ pushId: PropTypes.number.isRequired,
+ pushTimestamp: PropTypes.number.isRequired,
+ author: PropTypes.string.isRequired,
+ revision: PropTypes.string.isRequired,
+ jobCounts: PropTypes.object,
+ loggedIn: PropTypes.bool,
+ isStaff: PropTypes.bool,
+ repoName: PropTypes.string.isRequired,
+ isTryRepo: PropTypes.bool,
+ urlBasePath: PropTypes.string,
+ $injector: PropTypes.object.isRequired,
+ runnableVisible: PropTypes.bool.isRequired,
+ showRunnableJobsCb: PropTypes.func.isRequired,
+ hideRunnableJobsCb: PropTypes.func.isRequired,
+};
+
diff --git a/ui/job-view/PushJobs.jsx b/ui/job-view/PushJobs.jsx
index 050519913..ce47b82c4 100644
--- a/ui/job-view/PushJobs.jsx
+++ b/ui/job-view/PushJobs.jsx
@@ -1,43 +1,46 @@
import React from 'react';
import PropTypes from 'prop-types';
import * as _ from 'lodash';
-import { actions, store } from './redux/store';
import { platformMap } from '../js/constants';
import * as aggregateIds from './aggregateIds';
import Platform from './Platform';
+import { findInstance, findSelectedInstance, findJobInstance } from '../helpers/jobHelper';
export default class PushJobs extends React.Component {
constructor(props) {
super(props);
- this.$rootScope = this.props.$injector.get('$rootScope');
- this.$location = this.props.$injector.get('$location');
- this.thEvents = this.props.$injector.get('thEvents');
- this.ThResultSetStore = this.props.$injector.get('ThResultSetStore');
- this.ThJobModel = this.props.$injector.get('ThJobModel');
- this.thUrl = this.props.$injector.get('thUrl');
- this.thJobFilters = this.props.$injector.get('thJobFilters');
- this.thResultStatus = this.props.$injector.get('thResultStatus');
- this.thResultStatusInfo = this.props.$injector.get('thResultStatusInfo');
+ const { $injector, push, repoName } = this.props;
- this.rsMap = null;
- this.pushId = this.props.push.id;
- this.aggregateId = aggregateIds.getResultsetTableId(
- this.$rootScope.repoName,
+ this.$rootScope = $injector.get('$rootScope');
+ this.$location = $injector.get('$location');
+ this.thEvents = $injector.get('thEvents');
+ this.ThResultSetStore = $injector.get('ThResultSetStore');
+ this.ThJobModel = $injector.get('ThJobModel');
+ this.thUrl = $injector.get('thUrl');
+ this.thJobFilters = $injector.get('thJobFilters');
+
+ this.pushId = push.id;
+ this.aggregateId = aggregateIds.getPushTableId(
+ repoName,
this.pushId,
- this.props.push.revision
+ push.revision
);
- this.state = { platforms: null, isRunnableVisible: false };
+
this.onMouseDown = this.onMouseDown.bind(this);
this.selectJob = this.selectJob.bind(this);
- const showDuplicateJobs = this.$location.search().duplicate_jobs === 'visible';
- const expanded = this.$location.search().group_state === 'expanded';
- store.dispatch(actions.pushes.setCountExpanded(expanded));
- store.dispatch(actions.pushes.setShowDuplicates(showDuplicateJobs));
+ this.state = {
+ platforms: null,
+ isRunnableVisible: false,
+ };
+ }
+
+ componentWillMount() {
+ this.applyNewJobs();
}
componentDidMount() {
- this.$rootScope.$on(
+ this.applyNewJobsUnlisten = this.$rootScope.$on(
this.thEvents.applyNewJobs, (ev, appliedpushId) => {
if (appliedpushId === this.pushId) {
this.applyNewJobs();
@@ -45,54 +48,61 @@ export default class PushJobs extends React.Component {
}
);
- this.$rootScope.$on(
- this.thEvents.clearSelectedJob, () => {
- store.dispatch(actions.pushes.setSelectedJobId(null));
- }
- );
-
- this.$rootScope.$on(
+ this.globalFilterChangedUnlisten = this.$rootScope.$on(
this.thEvents.globalFilterChanged, () => {
this.filterJobs();
}
);
- this.$rootScope.$on(
+ this.groupStateChangedUnlisten = this.$rootScope.$on(
this.thEvents.groupStateChanged, () => {
this.filterJobs();
}
);
- this.$rootScope.$on(
+ this.jobsClassifiedUnlisten = this.$rootScope.$on(
this.thEvents.jobsClassified, () => {
this.filterJobs();
}
);
- this.$rootScope.$on(
+ this.searchPageUnlisten = this.$rootScope.$on(
this.thEvents.searchPage, () => {
this.filterJobs();
}
);
- this.$rootScope.$on(this.thEvents.showRunnableJobs, (ev, push) => {
- if (this.props.push.id === push.id) {
+ this.showRunnableJobsUnlisten = this.$rootScope.$on(this.thEvents.showRunnableJobs, (ev, pushId) => {
+ const { push, repoName } = this.props;
+
+ if (push.id === pushId) {
push.isRunnableVisible = true;
this.setState({ isRunnableVisible: true });
- this.ThResultSetStore.addRunnableJobs(this.$rootScope.repoName, push);
+ this.ThResultSetStore.addRunnableJobs(repoName, push);
}
});
- this.$rootScope.$on(this.thEvents.deleteRunnableJobs, (ev, push) => {
- if (this.props.push.id === push.id) {
+ this.deleteRunnableJobsUnlisten = this.$rootScope.$on(this.thEvents.deleteRunnableJobs, (ev, pushId) => {
+ const { push } = this.props;
+
+ if (push.id === pushId) {
push.isRunnableVisible = false;
this.setState({ isRunnableVisible: false });
- store.dispatch(actions.pushes.setSelectedRunnableJobs(null));
this.applyNewJobs();
}
});
}
+ componentWillUnmount() {
+ this.applyNewJobsUnlisten();
+ this.globalFilterChangedUnlisten();
+ this.groupStateChangedUnlisten();
+ this.jobsClassifiedUnlisten();
+ this.searchPageUnlisten();
+ this.showRunnableJobsUnlisten();
+ this.deleteRunnableJobsUnlisten();
+ }
+
onMouseDown(ev) {
const jobElem = ev.target.attributes.getNamedItem('data-job-id');
if (jobElem) {
@@ -105,14 +115,14 @@ export default class PushJobs extends React.Component {
} else if (job.state === 'runnable') { // Toggle runnable
this.handleRunnableClick(job);
} else {
- this.selectJob(job); // Left click
+ this.selectJob(job, ev.target); // Left click
}
}
}
getIdForPlatform(platform) {
return aggregateIds.getPlatformRowId(
- this.$rootScope.repoName,
+ this.props.repoName,
this.props.push.id,
platform.name,
platform.option
@@ -120,7 +130,7 @@ export default class PushJobs extends React.Component {
}
getJobFromId(jobId) {
- const jobMap = this.ThResultSetStore.getJobMap(this.$rootScope.repoName);
+ const jobMap = this.ThResultSetStore.getJobMap(this.props.repoName);
return jobMap[`${jobId}`].job_obj;
}
@@ -132,18 +142,22 @@ export default class PushJobs extends React.Component {
this.setState({ platforms });
}
- selectJob(job) {
- store.dispatch(actions.pushes.setSelectedJobId(job.id));
+ selectJob(job, el) {
+ const selected = findSelectedInstance();
+ if (selected) selected.setSelected(false);
+ const jobInstance = findInstance(el);
+ jobInstance.setSelected(true);
this.$rootScope.$emit(this.thEvents.jobClick, job);
}
applyNewJobs() {
- this.rsMap = this.ThResultSetStore.getResultSetsMap(this.$rootScope.repoName);
- if (!this.rsMap[this.pushId] || !this.rsMap[this.pushId].rs_obj.platforms) {
+ const { push } = this.props;
+
+ if (!push.platforms) {
return;
}
- const rsPlatforms = this.rsMap[this.pushId].rs_obj.platforms;
+ const rsPlatforms = push.platforms;
const platforms = rsPlatforms.reduce((acc, platform) => {
const thisPlatform = { ...platform };
thisPlatform.id = this.getIdForPlatform(platform);
@@ -162,7 +176,7 @@ export default class PushJobs extends React.Component {
handleLogViewerClick(jobId) {
// Open logviewer in a new window
this.ThJobModel.get(
- this.$rootScope.repoName,
+ this.props.repoName,
jobId
).then((data) => {
if (data.logs.length > 0) {
@@ -173,12 +187,12 @@ export default class PushJobs extends React.Component {
}
handleRunnableClick(job) {
- const selected = this.ThResultSetStore.toggleSelectedRunnableJob(
- this.$rootScope.repoName,
+ this.ThResultSetStore.toggleSelectedRunnableJob(
+ this.props.repoName,
this.pushId,
job.ref_data_name
);
- store.dispatch(actions.pushes.setSelectedRunnableJobs({ selectedRunnableJobs: selected }));
+ findJobInstance(job.id, false).toggleRunnableSelected();
}
filterPlatform(platform) {
@@ -187,9 +201,8 @@ export default class PushJobs extends React.Component {
group.visible = false;
group.jobs.forEach((job) => {
job.visible = this.thJobFilters.showJob(job);
- if (this.rsMap && job.state === 'runnable') {
- job.visible = job.visible &&
- this.rsMap[job.result_set_id].rs_obj.isRunnableVisible;
+ if (job.state === 'runnable') {
+ job.visible = job.visible && this.props.push.isRunnableVisible;
}
job.selected = this.$rootScope.selectedJob ? job.id === this.$rootScope.selectedJob.id : false;
if (job.visible) {
@@ -203,6 +216,8 @@ export default class PushJobs extends React.Component {
render() {
const platforms = this.state.platforms || {};
+ const { $injector, repoName } = this.props;
+
return (
@@ -210,7 +225,8 @@ export default class PushJobs extends React.Component {
platforms[id].visible &&
{
+ const pushList = this.ThResultSetStore.getResultSetsArray(repoName);
+ this.$timeout(() => {
+ this.setState({ pushList, loadingPushes: false });
+ }, 0);
+ });
+
+ this.jobsLoadedUnlisten = this.$rootScope.$on(this.thEvents.jobsLoaded, () => {
+ const pushList = this.ThResultSetStore.getResultSetsArray(repoName);
+ this.$timeout(() => {
+ this.setState({ pushList, jobsReady: true });
+ }, 0);
+ });
+
+ this.jobClickUnlisten = this.$rootScope.$on(this.thEvents.jobClick, (ev, job) => {
+ this.$location.search('selectedJob', job.id);
+ const { repoName } = this.props;
+ if (repoName) {
+ this.ThResultSetStore.setSelectedJob(repoName, job);
+ }
+ });
+
+ this.clearSelectedJobUnlisten = this.$rootScope.$on(this.thEvents.clearSelectedJob, () => {
+ this.$location.search('selectedJob', null);
+ });
+
+ this.changeSelectionUnlisten = this.$rootScope.$on(
+ this.thEvents.changeSelection, (ev, direction, jobNavSelector) => {
+ this.changeSelectedJob(ev, direction, jobNavSelector);
+ }
+ );
+
+ this.jobsLoadedUnlisten = this.$rootScope.$on(
+ this.thEvents.jobsLoaded, () => {
+ const selectedJobId = parseInt(this.$location.search().selectedJob);
+ if (selectedJobId) {
+ this.setSelectedJobFromQueryString(selectedJobId);
+ }
+ }
+ );
+ }
+
+ componentWillUnmount() {
+ this.pushesLoadedUnlisten();
+ this.jobsLoadedUnlisten();
+ this.jobClickUnlisten();
+ this.clearSelectedJobUnlisten();
+ this.changeSelectionUnlisten();
+ this.jobsLoadedUnlisten();
+ }
+
+ getNextPushes(count, keepFilters) {
+ this.setState({ loadingPushes: true });
+ const revision = this.$location.search().revision;
+ if (revision) {
+ this.$rootScope.skipNextPageReload = true;
+ this.$location.search('revision', null);
+ this.$location.search('tochange', revision);
+ }
+ this.ThResultSetStore.fetchResultSets(this.props.repoName, count, keepFilters)
+ .then(this.updateUrlFromchange);
+
+ }
+
+ /**
+ * If the URL has a query string param of ``selectedJob`` then select
+ * that job on load.
+ *
+ * If that job isn't in any of the loaded pushes, then throw
+ * an error and provide a link to load it with the right push.
+ */
+ setSelectedJobFromQueryString(selectedJobId) {
+ const { repoName } = this.props;
+ const { urlBasePath } = this.$rootScope;
+ const jobMap = this.ThResultSetStore.getJobMap(repoName);
+ const selectedJobEl = jobMap[`${selectedJobId}`];
+
+ // select the job in question
+ if (selectedJobEl) {
+ this.$rootScope.$emit(this.thEvents.jobClick, selectedJobEl.job_obj);
+ } else {
+ // If the ``selectedJob`` was not mapped, then we need to notify
+ // the user it's not in the range of the current result set list.
+ this.ThJobModel.get(repoName, selectedJobId).then((job) => {
+ this.ThResultSetModel.getResultSet(repoName, job.result_set_id).then((push) => {
+ const url = `${urlBasePath}?repo=${repoName}&revision=${push.data.revision}&selectedJob=${selectedJobId}`;
+
+ // the job exists, but isn't in any loaded push.
+ // provide a message and link to load the right push
+ this.thNotify.send(`Selected job id: ${selectedJobId} not within current push range.`,
+ 'danger',
+ { sticky: true, linkText: 'Load push', url });
+
+ });
+ }, function () {
+ // the job wasn't found in the db. Either never existed,
+ // or was expired and deleted.
+ this.thNotify.send(
+ `Unable to find job with id ${selectedJobId}`,
+ 'danger',
+ { sticky: true });
+ });
+ }
+ }
+
+ updateUrlFromchange() {
+ // since we fetched more pushes, we need to persist the
+ // push state in the URL.
+ const rsArray = this.ThResultSetStore.getResultSetsArray(this.props.repoName);
+ const updatedLastRevision = _.last(rsArray).revision;
+ if (this.$location.search().fromchange !== updatedLastRevision) {
+ this.$rootScope.skipNextPageReload = true;
+ this.$location.search('fromchange', updatedLastRevision);
+ }
+ }
+
+ changeSelectedJob(ev, direction, jobNavSelector) {
+ const jobMap = this.ThResultSetStore.getJobMap(this.props.repoName);
+ // Get the appropriate next index based on the direction and current job
+ // selection (if any). Must wrap end to end.
+ const getIndex = direction === 'next' ?
+ (idx, jobs) => (idx + 1 > jobs.length - 1 ? 0 : idx + 1) :
+ (idx, jobs) => (idx - 1 < 0 ? jobs.length - 1 : idx - 1);
+
+ // TODO: Move from using jquery here to using the ReactJS state tree (bug 1434679)
+ // to find the next/prev component to select so that setState can be called
+ // on the component directly.
+ //
+ // Filter the list of possible jobs down to ONLY ones in the .th-view-content
+ // div (excluding pinboard) and then to the specific selector passed
+ // in. And then to only VISIBLE (not filtered away) jobs. The exception
+ // is for the .selected-job. If that's not visible, we still want to
+ // include it, because it is the anchor from which we find
+ // the next/previous job.
+ //
+ // The .selected-job can be invisible, for instance, when filtered to
+ // unclassified failures only, and you then classify the selected job.
+ // It's still selected, but no longer visible.
+ const jobs = $('.th-view-content')
+ .find(jobNavSelector.selector)
+ .filter(':visible, .selected-job');
+
+ if (jobs.length) {
+ const selectedEl = jobs.filter('.selected-job').first();
+ const selIdx = jobs.index(selectedEl);
+ const idx = getIndex(selIdx, jobs);
+ const jobEl = $(jobs[idx]);
+
+ if (selectedEl.length) {
+ const selected = findInstance(selectedEl[0]);
+ selected.setSelected(false);
+ }
+
+ const nextSelected = findInstance(jobEl[0]);
+ nextSelected.setSelected(true);
+
+ const jobId = jobEl.attr('data-job-id');
+
+ if (jobMap && jobMap[jobId] && selIdx !== idx) {
+ this.selectJob(jobMap[jobId].job_obj, jobEl);
+ return;
+ }
+ }
+ // if there was no new job selected, then ensure that we clear any job that
+ // was previously selected.
+ if ($('.selected-job').css('display') === 'none') {
+ this.$rootScope.closeJob();
+ }
+ }
+
+ selectJob(job, jobEl) {
+ // Delay switching jobs right away, in case the user is switching rapidly between jobs
+ scrollToElement(jobEl);
+ if (this.jobChangedTimeout) {
+ window.clearTimeout(this.jobChangedTimeout);
+ }
+ this.jobChangedTimeout = window.setTimeout(() => {
+ this.$rootScope.$emit(this.thEvents.jobClick, job);
+ }, 200);
+ }
+
+ // Clear the job if it occurs in a particular area
+ clearJobOnClick(event) {
+ // Suppress for various UI elements so selection is preserved
+ const ignoreClear = event.target.hasAttribute("data-ignore-job-clear-on-click");
+
+ if (!ignoreClear && !this.thPinboard.hasPinnedJobs()) {
+ const selected = findSelectedInstance();
+ if (selected) {
+ selected.setSelected(false);
+ }
+ this.$timeout(this.$rootScope.closeJob);
+ }
+ }
+
+ render() {
+ const { $injector, user, repoName, revision } = this.props;
+ const currentRepo = this.props.currentRepo || {};
+ const { pushList, loadingPushes, jobsReady } = this.state;
+ const { loggedin, is_staff } = user;
+
+ return (
+
+ {jobsReady &&
}
+ {repoName && pushList.map(push => (
+
+ ))}
+ {loadingPushes &&
+
+ }
+ {pushList.length === 0 && !loadingPushes &&
+
+ }
+
+
get next:
+
+ {[10, 20, 50].map(count => (
+
(this.getNextPushes(count, true))}
+ key={count}
+ >{count}
+ ))}
+
+
+
+ );
+ }
+}
+
+treeherder.directive('pushList', ['reactDirective', '$injector',
+ (reactDirective, $injector) => reactDirective(
+ PushList,
+ ['repoName', 'user', 'revision', 'currentRepo'],
+ {},
+ { $injector }
+ )
+]);
diff --git a/ui/job-view/PushLoadErrors.jsx b/ui/job-view/PushLoadErrors.jsx
new file mode 100644
index 000000000..c0ae1a10f
--- /dev/null
+++ b/ui/job-view/PushLoadErrors.jsx
@@ -0,0 +1,56 @@
+const PushLoadErrors = (props) => {
+ const { loadingPushes, currentRepo, revision, repoName } = props;
+ const urlParams = new URLSearchParams(location.hash.split('?')[1]);
+ urlParams.delete("revision");
+
+ return (
+
+ {loadingPushes && revision && currentRepo && currentRepo.url &&
+
+
+ {revision &&
+
+ Waiting for a push with revision {revision}
+ (view pushlog)
+
+ If the push exists, it will appear in a few minutes once it has been processed.
+
+ }
+
+
+ }
+ {!loadingPushes && revision &&
+
+ This is an invalid or unknown revision. Please change it, or click
+
here to reload the latest revisions from {repoName}.
+
+ }
+ {!loadingPushes && !revision && currentRepo &&
+
+
+ No pushes found.
+ No commit information could be loaded for this repository.
+ More information about this repository can be found here.
+
+
+ }
+ {!loadingPushes && !Object.keys(currentRepo).length &&
+
+ }
+
+ );
+};
+
+export default PushLoadErrors;
diff --git a/ui/job-view/Repo.jsx b/ui/job-view/Repo.jsx
deleted file mode 100644
index c30d2c32e..000000000
--- a/ui/job-view/Repo.jsx
+++ /dev/null
@@ -1,194 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { actions, store } from './redux/store';
-
-class Repo extends React.PureComponent {
-
- // While this component doesn't render anything, it acts as a top-level,
- // single-point for some events so that they don't happen for each push
- // object that is rendered.
-
- // TODO: Once we migrate the ng-repeat to ReactJS for the list of
- // pushes, this component will do the rendering of that array.
- // This component could arguably be renamed as PushList at that time.
- // (bug 1434677).
-
- constructor(props) {
- super(props);
- this.$rootScope = this.props.$injector.get('$rootScope');
- this.$location = this.props.$injector.get('$location');
- this.thEvents = this.props.$injector.get('thEvents');
- this.ThResultSetStore = this.props.$injector.get('ThResultSetStore');
- this.thNotify = this.props.$injector.get('thNotify');
- this.thNotify = this.props.$injector.get('thNotify');
- this.ThJobModel = this.props.$injector.get('ThJobModel');
- this.ThResultSetModel = this.props.$injector.get('ThResultSetModel');
- }
-
- componentDidMount() {
- this.$rootScope.$on(this.thEvents.jobClick, (ev, job) => {
- this.$location.search('selectedJob', job.id);
- this.ThResultSetStore.setSelectedJob(this.$rootScope.repoName, job);
- store.dispatch(actions.pushes.setSelectedJobId(job.id));
- });
-
- this.$rootScope.$on(this.thEvents.clearSelectedJob, () => {
- this.$location.search('selectedJob', null);
- });
-
- this.$rootScope.$on(
- this.thEvents.changeSelection, (ev, direction, jobNavSelector) => {
- this.changeSelectedJob(ev, direction, jobNavSelector);
- }
- );
-
- this.$rootScope.$on(
- this.thEvents.jobsLoaded, () => {
- const selectedJobId = parseInt(this.$location.search().selectedJob);
- if (selectedJobId) {
- this.setSelectedJobFromQueryString(selectedJobId);
- }
- }
- );
- }
-
- /**
- * If the URL has a query string param of ``selectedJob`` then select
- * that job on load.
- *
- * If that job isn't in any of the loaded resultsets, then throw
- * an error and provide a link to load it with the right resultset.
- */
- setSelectedJobFromQueryString(selectedJobId) {
- const jobMap = this.ThResultSetStore.getJobMap(this.$rootScope.repoName);
- const selectedJobEl = jobMap[`${selectedJobId}`];
-
- // select the job in question
- if (selectedJobEl) {
- const jobSelector = `button[data-job-id='${selectedJobId}']`;
- this.$rootScope.$emit(this.thEvents.jobClick, selectedJobEl.job_obj);
- // scroll to make it visible
- this.scrollToElement($('.th-view-content').find(jobSelector).first());
- } else {
- // If the ``selectedJob`` was not mapped, then we need to notify
- // the user it's not in the range of the current result set list.
- this.ThJobModel.get(this.$rootScope.repoName, selectedJobId).then((job) => {
- this.ThResultSetModel.getResultSet(this.$rootScope.repoName, job.result_set_id).then(function (resultset) {
- const url = `${this.$rootScope.urlBasePath}?repo=${this.$rootScope.repoName}&revision=${resultset.data.revision}&selectedJob=${selectedJobId}`;
-
- // the job exists, but isn't in any loaded resultset.
- // provide a message and link to load the right resultset
- this.thNotify.send(`Selected job id: ${selectedJobId} not within current push range.`,
- 'danger',
- { sticky: true, linkText: 'Load push', url });
-
- });
- }, function () {
- // the job wasn't found in the db. Either never existed,
- // or was expired and deleted.
- this.thNotify.send(`Unable to find job with id ${selectedJobId}`, 'danger', { sticky: true });
- });
- }
- }
-
- changeSelectedJob(ev, direction, jobNavSelector) {
- const jobMap = this.ThResultSetStore.getJobMap(this.$rootScope.repoName);
- // Get the appropriate next index based on the direction and current job
- // selection (if any). Must wrap end to end.
- const getIndex = direction === 'next' ?
- (idx, jobs) => (idx + 1 > jobs.length - 1 ? 0 : idx + 1) :
- (idx, jobs) => (idx - 1 < 0 ? jobs.length - 1 : idx - 1);
-
- // TODO: Move from using jquery here to using the ReactJS state tree (bug 1434679)
- // to find the next/prev component to select so that setState can be called
- // on the component directly.
- //
- // Filter the list of possible jobs down to ONLY ones in the .th-view-content
- // div (excluding pinboard) and then to the specific selector passed
- // in. And then to only VISIBLE (not filtered away) jobs. The exception
- // is for the .selected-job. If that's not visible, we still want to
- // include it, because it is the anchor from which we find
- // the next/previous job.
- //
- // The .selected-job can be invisible, for instance, when filtered to
- // unclassified failures only, and you then classify the selected job.
- // It's still selected, but no longer visible.
- const jobs = $('.th-view-content').find(jobNavSelector.selector).filter(':visible, .selected-job, .selected-count');
-
- if (jobs.length) {
- const selIdx = jobs.index(jobs.filter('.selected-job, .selected-count').first());
- const idx = getIndex(selIdx, jobs);
- const jobEl = $(jobs[idx]);
- const jobId = jobEl.attr('data-job-id');
-
- if (jobMap && jobMap[jobId] && selIdx !== idx) {
- this.selectJob(jobMap[jobId].job_obj, jobEl);
- return;
- }
- }
- // if there was no new job selected, then ensure that we clear any job that
- // was previously selected.
- if ($('.selected-job').css('display') === 'none') {
- this.$rootScope.closeJob();
- }
- }
-
- // TODO: see if Element.scrollIntoView() can be used here. (bug 1434679)
- scrollToElement(el, duration) {
- if (_.isUndefined(duration)) {
- duration = 50;
- }
- if (el.position() !== undefined) {
- var scrollOffset = -50;
- if (window.innerHeight >= 500 && window.innerHeight < 1000) {
- scrollOffset = -100;
- } else if (window.innerHeight >= 1000) {
- scrollOffset = -200;
- }
- if (!this.isOnScreen(el)) {
- $('.th-global-content').scrollTo(el, duration, { offset: scrollOffset });
- }
- }
- }
-
- isOnScreen(el) {
- const viewport = {};
- viewport.top = $(window).scrollTop() + $('#global-navbar-container').height() + 30;
- const filterbarheight = $('.active-filters-bar').height();
- viewport.top = filterbarheight > 0 ? viewport.top + filterbarheight : viewport.top;
- const updatebarheight = $('.update-alert-panel').height();
- viewport.top = updatebarheight > 0 ? viewport.top + updatebarheight : viewport.top;
- viewport.bottom = $(window).height() - $('#info-panel').height() - 20;
- const bounds = {};
- bounds.top = el.offset().top;
- bounds.bottom = bounds.top + el.outerHeight();
- return ((bounds.top <= viewport.bottom) && (bounds.bottom >= viewport.top));
- }
-
- selectJob(job, jobEl) {
- // Delay switching jobs right away, in case the user is switching rapidly between jobs
- store.dispatch(actions.pushes.setSelectedJobId(job.id));
- this.scrollToElement(jobEl);
- if (this.jobChangedTimeout) {
- window.clearTimeout(this.jobChangedTimeout);
- }
- this.jobChangedTimeout = window.setTimeout(() => {
- this.$rootScope.$emit(this.thEvents.jobClick, job);
- }, 200);
- }
-
- render() {
- return null;
- }
-}
-
-Repo.propTypes = {
- $injector: PropTypes.object.isRequired,
-};
-
-// Need store here because this React component is not wrapped in a .
-// Once we're completely on React, the entire app will be wrapped in a singular
-// so all components will get the store.
-treeherder.constant('store', store);
-treeherder.directive('repo', ['reactDirective', '$injector', (reactDirective, $injector) =>
- reactDirective(Repo, ['repo'], {}, { $injector })]);
diff --git a/ui/job-view/Revision.jsx b/ui/job-view/Revision.jsx
index 935e29528..312fe3633 100644
--- a/ui/job-view/Revision.jsx
+++ b/ui/job-view/Revision.jsx
@@ -1,4 +1,5 @@
import PropTypes from 'prop-types';
+import { parseAuthor } from '../helpers/revisionHelper';
export const Initials = (props) => {
const str = props.author || '';
@@ -27,34 +28,31 @@ export const Initials = (props) => {
export class Revision extends React.PureComponent {
constructor(props) {
super(props);
- this.userTokens = this.props.revision.author.split(/[<>]+/);
- this.name = this.userTokens[0].trim().replace(/\w\S*/g,
- txt => txt.charAt(0).toUpperCase() + txt.substr(1));
- if (this.userTokens.length > 1) this.email = this.userTokens[1];
- const comment = this.props.revision.comments.split('\n')[0];
- const escapedComment = _.escape(comment);
- this.escapedCommentHTML = { __html: this.props.linkifyBugsFilter(escapedComment) };
+ const { revision, linkifyBugsFilter } = this.props;
- this.tags = '';
- if (escapedComment.search('Backed out') >= 0 ||
- escapedComment.search('Back out') >= 0) {
- this.tags += 'backout';
- }
+ const escapedComment = _.escape(revision.comments.split('\n')[0]);
+ this.escapedCommentHTML = { __html: linkifyBugsFilter(escapedComment) };
+ this.tags = escapedComment.search('Backed out') >= 0 || escapedComment.search('Back out') >= 0 ?
+ 'backout' : '';
}
render() {
+ const { revision, repo } = this.props;
+ const { name, email } = parseAuthor(revision.author);
+ const commitRevision = revision.revision;
+
return (
{this.props.revision.revision.substring(0, 12)}
+ >{commitRevision.substring(0, 12)}
-
diff --git a/ui/job-view/RevisionList.jsx b/ui/job-view/RevisionList.jsx
index e109df31c..e528ba842 100644
--- a/ui/job-view/RevisionList.jsx
+++ b/ui/job-view/RevisionList.jsx
@@ -9,21 +9,23 @@ export class RevisionList extends React.PureComponent {
}
render() {
+ const { push, repo } = this.props;
+
return (
- {this.props.push.revisions.map((revision, i) =>
+ {push.revisions.map((revision, i) =>
()
)}
{this.hasMore &&
}
@@ -43,9 +45,7 @@ export const MoreRevisionsLink = props => (
{`\u2026and more`}
-
-
+ >{`\u2026and more`}
);
diff --git a/ui/job-view/aggregateIds.js b/ui/job-view/aggregateIds.js
index c03d36fdd..bf832b14b 100644
--- a/ui/job-view/aggregateIds.js
+++ b/ui/job-view/aggregateIds.js
@@ -1,20 +1,17 @@
-export const escape = function (id) {
- return id.replace(/(:|\[|\]|\?|,|\.|\s+)/g, '-');
-};
+export const escape = id => (
+ id.replace(/(:|\[|\]|\?|,|\.|\s+)/g, '-')
+);
-export const getPlatformRowId = function (repoName, resultsetId, platformName, platformOptions) {
+export const getPlatformRowId = (repoName, pushId, platformName, platformOptions) => (
// ensure there are no invalid characters in the id (like spaces, etc)
- return escape(repoName +
- resultsetId +
- platformName +
- platformOptions);
-};
+ escape(repoName + pushId + platformName + platformOptions)
+);
-export const getResultsetTableId = function (repoName, resultsetId, revision) {
- return escape(repoName + resultsetId + revision);
-};
+export const getPushTableId = (repoName, pushId, revision) => (
+ escape(repoName + pushId + revision)
+);
-export const getGroupMapKey = function (result_set_id, grSymbol, grTier, plName, plOpt) {
+export const getGroupMapKey = (pushId, grSymbol, grTier, plName, plOpt) => (
//Build string key for groupMap entries
- return escape(result_set_id + grSymbol + grTier + plName + plOpt);
-};
+ escape(pushId + grSymbol + grTier + plName + plOpt)
+);
diff --git a/ui/job-view/redux/configureStore.js b/ui/job-view/redux/configureStore.js
deleted file mode 100644
index f7b7befb4..000000000
--- a/ui/job-view/redux/configureStore.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import { createStore, bindActionCreators, combineReducers } from 'redux';
-import * as pushes from './modules/pushes';
-
-export default () => {
- const reducer = combineReducers({
- pushes: pushes.reducer,
- });
- const store = createStore(reducer);
- const actions = {
- pushes: bindActionCreators(pushes.actions, store.dispatch),
- };
-
- return { store, actions };
-};
diff --git a/ui/job-view/redux/modules/pushes.js b/ui/job-view/redux/modules/pushes.js
deleted file mode 100644
index 404b2388a..000000000
--- a/ui/job-view/redux/modules/pushes.js
+++ /dev/null
@@ -1,60 +0,0 @@
-export const types = {
- SET_COUNTS_EXPANDED: 'SET_COUNTS_EXPANDED',
- SET_SHOW_DUPLICATES: 'SET_SHOW_DUPLICATES',
- SET_SELECTED_JOB: 'SET_SELECTED_JOB',
- SET_SELECTED_RUNNABLE_JOBS: 'SET_SELECTED_RUNNABLE_JOBS',
-};
-
-export const actions = {
- setSelectedJobId: jobId => ({
- type: types.SET_SELECTED_JOB,
- payload: {
- jobId,
- }
- }),
- setSelectedRunnableJobs: selectedRunnableJobs => ({
- type: types.SET_SELECTED_RUNNABLE_JOBS,
- payload: {
- selectedRunnableJobs,
- }
- }),
- setCountExpanded: countsExpanded => ({
- type: types.SET_COUNTS_EXPANDED,
- payload: {
- countsExpanded
- }
- }),
- setShowDuplicates: showDuplicates => ({
- type: types.SET_SHOW_DUPLICATES,
- payload: {
- showDuplicates
- }
- }),
-
-};
-
-const initialState = {
- selectedJobId: null,
- selectedRunnableJobs: null,
- countsExpanded: false,
- showDuplicates: false,
-};
-
-export const reducer = (state = initialState, action) => {
- switch (action.type) {
- case types.SET_SELECTED_JOB:
- return {
- ...state,
- selectedJobId: action.payload.jobId
- };
- case types.SET_SELECTED_RUNNABLE_JOBS:
- case types.SET_SHOW_DUPLICATES:
- case types.SET_COUNTS_EXPANDED:
- return {
- ...state,
- ...action.payload
- };
- default:
- return state;
- }
-};
diff --git a/ui/job-view/redux/store.js b/ui/job-view/redux/store.js
deleted file mode 100644
index 3c3631c29..000000000
--- a/ui/job-view/redux/store.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import configureStore from './configureStore';
-
-export const { store, actions } = configureStore();
diff --git a/ui/js/controllers/jobs.js b/ui/js/controllers/jobs.js
deleted file mode 100644
index 09314d374..000000000
--- a/ui/js/controllers/jobs.js
+++ /dev/null
@@ -1,272 +0,0 @@
-treeherderApp.controller('JobsCtrl', [
- '$scope', '$rootScope', '$routeParams',
- 'thDefaultRepo',
- 'ThResultSetStore', '$location', 'thEvents',
- 'thNotify',
- function JobsCtrl(
- $scope, $rootScope, $routeParams,
- thDefaultRepo,
- ThResultSetStore, $location, thEvents, thNotify) {
-
- // load our initial set of resultsets
- // scope needs this function so it can be called directly by the user, too.
- $scope.getNextResultSets = function (count, keepFilters) {
- var revision = $location.search().revision;
- if (revision) {
- $rootScope.skipNextPageReload = true;
- $location.search('revision', null);
- $location.search('tochange', revision);
- }
- ThResultSetStore.fetchResultSets($scope.repoName, count, keepFilters)
- .then(function () {
-
- // since we fetched more resultsets, we need to persist the
- // resultset state in the URL.
- var rsArray = ThResultSetStore.getResultSetsArray($scope.repoName);
- var updatedLastRevision = _.last(rsArray).revision;
- if ($location.search().fromchange !== updatedLastRevision) {
- $rootScope.skipNextPageReload = true;
- $location.search('fromchange', updatedLastRevision);
- }
- });
- };
-
- // set to the default repo if one not specified
- if ($routeParams.hasOwnProperty("repo") &&
- $routeParams.repo !== "") {
- $rootScope.repoName = $routeParams.repo;
- } else {
- $rootScope.repoName = thDefaultRepo;
- $location.search("repo", $rootScope.repoName);
- }
-
- ThResultSetStore.addRepository($scope.repoName);
-
- $scope.isLoadingRsBatch = ThResultSetStore.getLoadingStatus($scope.repoName);
- $scope.result_sets = ThResultSetStore.getResultSetsArray($scope.repoName);
- $scope.job_map = ThResultSetStore.getJobMap($scope.repoName);
-
- $scope.searchParams = $location.search();
- $scope.locationHasSearchParam = function (prop) {
- return _.has($scope.searchParams, prop);
- };
-
- $scope.getSearchParamValue = function (param) {
- var params = $location.search();
- var searchParamValue = params[param];
- // in the event the user manually strips off the search
- // parameter and its = sign, which would return true
- if (searchParamValue === true) {
- return "";
- }
- return searchParamValue;
- };
-
- if ($location.search().revision === 'undefined') {
- thNotify.send("Invalid value for revision parameter.", 'danger');
- }
-
- if (ThResultSetStore.isNotLoaded($scope.repoName)) {
- // get our first set of resultsets
- ThResultSetStore.fetchResultSets(
- $scope.repoName,
- ThResultSetStore.defaultResultSetCount,
- true);
- }
- }
-]);
-
-
-treeherderApp.controller('ResultSetCtrl', [
- '$scope', '$rootScope',
- 'thResultStatusInfo', 'thDateFormat',
- 'ThResultSetStore', 'thEvents', 'thNotify',
- 'thBuildApi', 'thPinboard', 'ThResultSetModel', 'dateFilter',
- 'ThModelErrors', 'ThTaskclusterErrors', '$uibModal', 'thPinboardCountError',
- function ResultSetCtrl(
- $scope, $rootScope,
- thResultStatusInfo, thDateFormat,
- ThResultSetStore, thEvents, thNotify,
- thBuildApi, thPinboard, ThResultSetModel, dateFilter, ThModelErrors,
- ThTaskclusterErrors, $uibModal, thPinboardCountError) {
-
- $scope.getCountClass = function (resultStatus) {
- return thResultStatusInfo(resultStatus).btnClass;
- };
- $scope.getCountText = function (resultStatus) {
- return thResultStatusInfo(resultStatus).countText;
- };
- $scope.viewJob = function (job) {
- // set the selected job
- $rootScope.selectedJob = job;
- };
-
- /**
- * Pin all jobs that pass the global filters.
- *
- * If optional resultsetId is passed in, then only pin jobs from that
- * resultset.
- */
- $scope.pinAllShownJobs = function () {
- if (!thPinboard.spaceRemaining()) {
- thNotify.send(thPinboardCountError, 'danger');
- return;
- }
- var shownJobs = ThResultSetStore.getAllShownJobs(
- $rootScope.repoName,
- thPinboard.spaceRemaining(),
- thPinboardCountError,
- $scope.resultset.id
- );
- thPinboard.pinJobs(shownJobs);
-
- if (!$rootScope.selectedJob) {
- $scope.viewJob(shownJobs[0]);
- }
-
- };
-
- $scope.showRunnableJobs = function () {
- if ($scope.user.loggedin) {
- $rootScope.$emit(thEvents.showRunnableJobs, $scope.resultset);
- }
- };
-
- $scope.deleteRunnableJobs = function () {
- $rootScope.$emit(thEvents.deleteRunnableJobs, $scope.resultset);
- };
-
- $scope.getCancelJobsTitle = function () {
- if (!$scope.user || !$scope.user.loggedin) {
- return "Must be logged in to cancel jobs";
- }
- return "Cancel all jobs";
- };
-
- $scope.canCancelJobs = function () {
- return $scope.user && $scope.user.loggedin;
- };
-
- $scope.confirmCancelAllJobs = function () {
- $scope.showConfirmCancelAll = true;
- };
-
- $scope.hideConfirmCancelAll = function () {
- $scope.showConfirmCancelAll = false;
- };
-
- $scope.cancelAllJobs = function (revision) {
- $scope.showConfirmCancelAll = false;
- if (!$scope.canCancelJobs()) return;
-
- ThResultSetModel.cancelAll($scope.resultset.id, $scope.repoName).then(function () {
- return thBuildApi.cancelAll($scope.repoName, revision);
- }).catch(function (e) {
- thNotify.send(
- ThModelErrors.format(e, "Failed to cancel all jobs"),
- 'danger', true
- );
- });
- };
-
- $scope.customPushAction = function () {
- $uibModal.open({
- templateUrl: 'partials/main/tcjobactions.html',
- controller: 'TCJobActionsCtrl',
- size: 'lg',
- resolve: {
- job: () => null,
- repoName: function () {
- return $scope.repoName;
- },
- resultsetId: function () {
- return $scope.resultset.id;
- }
- }
- });
- };
-
- $scope.triggerMissingJobs = function (revision) {
- if (!window.confirm('This will trigger all missing jobs for revision ' + revision + '!\n\nClick "OK" if you want to proceed.')) {
- return;
- }
-
- ThResultSetStore.getGeckoDecisionTaskId(
- $scope.repoName,
- $scope.resultset.id
- ).then(function (decisionTaskID) {
- ThResultSetModel.triggerMissingJobs(
- decisionTaskID
- ).then(function (msg) {
- thNotify.send(msg, "success");
- }, function (e) {
- thNotify.send(
- ThModelErrors.format(e, "The action 'trigger missing jobs' failed"),
- 'danger', true
- );
- });
- });
- };
-
- $scope.triggerAllTalosJobs = function (revision) {
- if (!window.confirm('This will trigger all talos jobs for revision ' + revision + '!\n\nClick "OK" if you want to proceed.')) {
- return;
- }
-
- var times = parseInt(window.prompt("Enter number of instances to have for each talos job", 6));
- while (times < 1 || times > 6 || isNaN(times)) {
- times = window.prompt("We only allow instances of each talos job to be between 1 to 6 times. Enter again", 6);
- }
-
- ThResultSetStore.getGeckoDecisionTaskId(
- $scope.repoName,
- $scope.resultset.id
- ).then(function (decisionTaskID) {
- ThResultSetModel.triggerAllTalosJobs(
- times,
- decisionTaskID
- ).then(function (msg) {
- thNotify.send(msg, "success");
- }, function (e) {
- thNotify.send(ThTaskclusterErrors.format(e), 'danger', { sticky: true });
- });
- });
- };
-
- $scope.showTriggerButton = function () {
- var buildernames = ThResultSetStore.getSelectedRunnableJobs($rootScope.repoName, $scope.resultset.id);
- return buildernames.length > 0;
- };
-
- $scope.triggerNewJobs = function () {
- if (!window.confirm(
- 'This will trigger all selected jobs. Click "OK" if you want to proceed.')) {
- return;
- }
- if ($scope.user.loggedin) {
- var buildernames = ThResultSetStore.getSelectedRunnableJobs($rootScope.repoName, $scope.resultset.id);
- ThResultSetStore.getGeckoDecisionTaskId($rootScope.repoName, $scope.resultset.id).then(function (decisionTaskID) {
- ThResultSetModel.triggerNewJobs(buildernames, decisionTaskID).then(function (result) {
- thNotify.send(result, "success");
- ThResultSetStore.deleteRunnableJobs($scope.repoName, $scope.resultset);
- }, function (e) {
- thNotify.send(ThTaskclusterErrors.format(e), 'danger', { sticky: true });
- });
- });
- } else {
- thNotify.send("Must be logged in to trigger a job", 'danger');
- }
- };
-
- $scope.revisionResultsetFilterUrl = $scope.urlBasePath + "?repo=" +
- $scope.repoName + "&revision=" +
- $scope.resultset.revision;
-
- $scope.resultsetDateStr = dateFilter($scope.resultset.push_timestamp*1000,
- thDateFormat);
-
- $scope.authorResultsetFilterUrl = $scope.urlBasePath + "?repo=" +
- $scope.repoName + "&author=" +
- encodeURIComponent($scope.resultset.author);
- }
-]);
diff --git a/ui/js/controllers/main.js b/ui/js/controllers/main.js
index 0297154fe..3cfdadd2d 100644
--- a/ui/js/controllers/main.js
+++ b/ui/js/controllers/main.js
@@ -26,6 +26,18 @@ treeherderApp.controller('MainCtrl', [
// Ensure user is available on initial page load
$rootScope.user = {};
+ // set to the default repo if one not specified
+ const repoName = $location.search().repo;
+ if (repoName) {
+ $rootScope.repoName = repoName;
+ } else {
+ $rootScope.repoName = thDefaultRepo;
+ $location.search("repo", $rootScope.repoName);
+ }
+ $rootScope.revision = $location.search().revision;
+ ThResultSetStore.addRepository($rootScope.repoName);
+
+
thClassificationTypes.load();
var checkServerRevision = function () {
@@ -41,9 +53,6 @@ treeherderApp.controller('MainCtrl', [
});
};
- // Trigger missing jobs is dangerous on repos other than these (see bug 1335506)
- $scope.triggerMissingRepos = ['mozilla-inbound', 'autoland'];
-
$scope.updateButtonClick = function () {
if (window.confirm("Reload the page to pick up Treeherder updates?")) {
window.location.reload(true);
@@ -138,23 +147,12 @@ treeherderApp.controller('MainCtrl', [
$rootScope.selectedJob = null;
// Clear the selected job display style
- $rootScope.$emit(thEvents.clearSelectedJob, $rootScope.selectedJob);
+ $rootScope.$emit(thEvents.clearSelectedJob);
// Reset selected job to null to initialize nav position
ThResultSetStore.setSelectedJob($rootScope.repoName);
};
- // Clear the job if it occurs in a particular area
- $scope.clearJobOnClick = function (event) {
- var element = event.target;
- // Suppress for various UI elements so selection is preserved
- var ignoreClear = element.hasAttribute("data-ignore-job-clear-on-click");
-
- if (!ignoreClear && !thPinboard.hasPinnedJobs()) {
- $scope.closeJob();
- }
- };
-
$scope.repoModel = ThRepositoryModel;
/**
diff --git a/ui/js/directives/treeherder/bottom_nav_panel.js b/ui/js/directives/treeherder/bottom_nav_panel.js
index ede5b842c..e89b00ad3 100644
--- a/ui/js/directives/treeherder/bottom_nav_panel.js
+++ b/ui/js/directives/treeherder/bottom_nav_panel.js
@@ -1,31 +1,31 @@
-treeherder.directive('thPinnedJob', [
- 'thResultStatusInfo', 'thResultStatus',
- function (thResultStatusInfo, thResultStatus) {
+import { getBtnClass, getStatus } from "../../../helpers/jobHelper";
- var getHoverText = function (job) {
- var duration = Math.round((job.end_timestamp - job.start_timestamp) / 60);
- var status = thResultStatus(job);
- return job.job_type_name + " - " + status + " - " + duration + "mins";
- };
+treeherder.directive('thPinnedJob', function () {
- return {
- restrict: "E",
- link: function (scope) {
- var unbindWatcher = scope.$watch("job", function () {
- var resultState = thResultStatus(scope.job);
- scope.job.display = thResultStatusInfo(resultState, scope.job.failure_classification_id);
- scope.hoverText = getHoverText(scope.job);
+ var getHoverText = function (job) {
+ var duration = Math.round((job.end_timestamp - job.start_timestamp) / 60);
+ var status = getStatus(job);
+ return job.job_type_name + " - " + status + " - " + duration + "mins";
+ };
- if (scope.job.state === "completed") {
- //Remove watchers when a job has a completed status
- unbindWatcher();
- }
+ return {
+ restrict: "E",
+ link: function (scope) {
+ var unbindWatcher = scope.$watch("job", function () {
+ var resultState = getStatus(scope.job);
+ scope.job.btnClass = getBtnClass(resultState, scope.job.failure_classification_id);
+ scope.hoverText = getHoverText(scope.job);
- }, true);
- },
- templateUrl: 'partials/main/thPinnedJob.html'
- };
- }]);
+ if (scope.job.state === "completed") {
+ //Remove watchers when a job has a completed status
+ unbindWatcher();
+ }
+
+ }, true);
+ },
+ templateUrl: 'partials/main/thPinnedJob.html'
+ };
+});
treeherder.directive('thRelatedBugQueued', function () {
diff --git a/ui/js/directives/treeherder/clonejobs.js b/ui/js/directives/treeherder/clonejobs.js
deleted file mode 100644
index e69de29bb..000000000
diff --git a/ui/js/directives/treeherder/resultsets.js b/ui/js/directives/treeherder/resultsets.js
deleted file mode 100644
index 593fb3eb2..000000000
--- a/ui/js/directives/treeherder/resultsets.js
+++ /dev/null
@@ -1,48 +0,0 @@
-treeherder.directive('thActionButton', function () {
- return {
- restrict: "E",
- templateUrl: 'partials/main/thActionButton.html'
- };
-});
-
-treeherder.directive('thResultCounts', [
- 'thEvents', '$rootScope', function (thEvents, $rootScope) {
-
- return {
- restrict: "E",
- link: function (scope) {
- var setTotalCount = function () {
- if (scope.resultset.job_counts) {
- scope.inProgress = scope.resultset.job_counts.pending +
- scope.resultset.job_counts.running;
- var total = scope.resultset.job_counts.completed + scope.inProgress;
- scope.percentComplete = total > 0 ?
- Math.floor(((scope.resultset.job_counts.completed / total) * 100)) : undefined;
- }
- };
-
- $rootScope.$on(thEvents.applyNewJobs, function (evt, resultSetId) {
- if (resultSetId === scope.resultset.id) {
- setTotalCount();
- }
- });
-
- },
- templateUrl: 'partials/main/thResultCounts.html'
- };
- }]);
-
-treeherder.directive('thAuthor', function () {
-
- return {
- restrict: "E",
- link: function (scope, element, attrs) {
- var authorMatch = attrs.author.match(/\<(.*?)\>+/);
- scope.authorEmail = authorMatch ? authorMatch[1] : attrs.author;
- },
- template: '' +
- '{{authorEmail}}'
- };
-});
-
diff --git a/ui/js/directives/treeherder/top_nav_bar.js b/ui/js/directives/treeherder/top_nav_bar.js
index 28c2c7264..529c63d3d 100644
--- a/ui/js/directives/treeherder/top_nav_bar.js
+++ b/ui/js/directives/treeherder/top_nav_bar.js
@@ -1,3 +1,5 @@
+import { getBtnClass } from '../../../helpers/jobHelper';
+
treeherder.directive('thWatchedRepo', [
'ThLog', 'ThRepositoryModel',
function (ThLog, ThRepositoryModel) {
@@ -152,15 +154,12 @@ treeherder.directive('thRepoMenuItem',
};
});
-treeherder.directive('thResultStatusChicklet', [
- 'thResultStatusInfo', function (thResultStatusInfo) {
- return {
- restrict: "E",
- link: function (scope) {
- scope.chickletClass = thResultStatusInfo(scope.filterName).btnClass +
- "-filter-chicklet";
- },
- templateUrl: 'partials/main/thResultStatusChicklet.html'
- };
- }
-]);
+treeherder.directive('thResultStatusChicklet', function () {
+ return {
+ restrict: "E",
+ link: function (scope) {
+ scope.chickletClass = `${getBtnClass(scope.filterName)}-filter-chicklet`;
+ },
+ templateUrl: 'partials/main/thResultStatusChicklet.html'
+ };
+});
diff --git a/ui/js/models/resultsets_store.js b/ui/js/models/resultsets_store.js
index 2d6a68d0b..154a248b4 100644
--- a/ui/js/models/resultsets_store.js
+++ b/ui/js/models/resultsets_store.js
@@ -348,9 +348,11 @@ treeherder.factory('ThResultSetStore', [
});
};
- var deleteRunnableJobs = function (repoName, resultSet) {
- repositories[repoName].rsMap[resultSet.id].selected_runnable_jobs = [];
- resultSet.isRunnableVisible = false;
+ var deleteRunnableJobs = function (repoName, pushId) {
+ const push = repositories[repoName].rsMap[pushId];
+ push.selected_runnable_jobs = [];
+ push.rs_obj.isRunnableVisible = false;
+ $rootScope.$emit(thEvents.selectRunnableJob, []);
$rootScope.$emit(thEvents.globalFilterChanged);
};
@@ -671,7 +673,7 @@ treeherder.factory('ThResultSetStore', [
revision = repositories[repoName].rsMap[resultsetId].rs_obj.revision;
- resultsetAggregateId = thAggregateIds.getResultsetTableId(
+ resultsetAggregateId = thAggregateIds.getPushTableId(
$rootScope.repoName, resultsetId, revision
);
@@ -806,21 +808,18 @@ treeherder.factory('ThResultSetStore', [
isInResultSetRange(repoName, data.results[i].push_timestamp) &&
repositories[repoName].rsMap[data.results[i].id] === undefined) {
- $log.debug("prepending resultset: ", data.results[i].id);
repositories[repoName].resultSets.push(data.results[i]);
added.push(data.results[i]);
- } else {
- $log.debug("not prepending. timestamp is older");
}
}
mapResultSets(repoName, added);
repositories[repoName].loadingStatus.prepending = false;
+ $rootScope.$emit(thEvents.pushesLoaded);
};
var appendResultSets = function (repoName, data) {
-
if (data.results.length > 0) {
$log.debug("appendResultSets", data.results);
@@ -850,6 +849,7 @@ treeherder.factory('ThResultSetStore', [
}
repositories[repoName].loadingStatus.appending = false;
+ $rootScope.$emit(thEvents.pushesLoaded);
};
/**
@@ -883,19 +883,18 @@ treeherder.factory('ThResultSetStore', [
return repositories[repoName].resultSets;
};
- var getResultSetsMap = function (repoName) {
- return repositories[repoName].rsMap;
- };
-
var getResultSet = function (repoName, resultsetId) {
return repositories[repoName].rsMap[resultsetId].rs_obj;
};
- var getSelectedRunnableJobs = function (repoName, resultsetId) {
- if (!repositories[repoName].rsMap[resultsetId].selected_runnable_jobs) {
- repositories[repoName].rsMap[resultsetId].selected_runnable_jobs = [];
+ var getSelectedRunnableJobs = function (repoName, pushId) {
+ if (!repositories[repoName].rsMap[pushId]) {
+ return 0;
}
- return repositories[repoName].rsMap[resultsetId].selected_runnable_jobs;
+ if (!repositories[repoName].rsMap[pushId].selected_runnable_jobs) {
+ repositories[repoName].rsMap[pushId].selected_runnable_jobs = [];
+ }
+ return repositories[repoName].rsMap[pushId].selected_runnable_jobs;
};
var getGeckoDecisionJob = function (repoName, resultsetId) {
@@ -948,27 +947,14 @@ treeherder.factory('ThResultSetStore', [
} else {
selectedRunnableJobs.splice(jobIndex, 1);
}
+ $rootScope.$emit(thEvents.selectRunnableJob, selectedRunnableJobs, resultsetId);
return selectedRunnableJobs;
};
- var isRunnableJobSelected = function (repoName, resultsetId, buildername) {
- var selectedRunnableJobs = getSelectedRunnableJobs(repoName, resultsetId);
- return selectedRunnableJobs.indexOf(buildername) !== -1;
- };
-
var getJobMap = function (repoName) {
// this is a "watchable" for jobs
return repositories[repoName].jobMap;
};
- var getGroupMap = function (repoName) {
- return repositories[repoName].grpMap;
- };
- var getLoadingStatus = function (repoName) {
- return repositories[repoName].loadingStatus;
- };
- var isNotLoaded = function (repoName) {
- return _.isEmpty(repositories[repoName].rsMap);
- };
var fetchResultSets = function (repoName, count, keepFilters) {
/**
@@ -1184,22 +1170,15 @@ treeherder.factory('ThResultSetStore', [
fetchResultSets: fetchResultSets,
getAllShownJobs: getAllShownJobs,
getJobMap: getJobMap,
- getGroupMap: getGroupMap,
- getLoadingStatus: getLoadingStatus,
- getPlatformKey: getPlatformKey,
addRunnableJobs: addRunnableJobs,
- isRunnableJobSelected: isRunnableJobSelected,
getSelectedRunnableJobs: getSelectedRunnableJobs,
- getGeckoDecisionJob: getGeckoDecisionJob,
getGeckoDecisionTaskId: getGeckoDecisionTaskId,
toggleSelectedRunnableJob: toggleSelectedRunnableJob,
getResultSet: getResultSet,
getResultSetsArray: getResultSetsArray,
- getResultSetsMap: getResultSetsMap,
getSelectedJob: getSelectedJob,
getFilteredUnclassifiedFailureCount: getFilteredUnclassifiedFailureCount,
getAllUnclassifiedFailureCount: getAllUnclassifiedFailureCount,
- isNotLoaded: isNotLoaded,
setSelectedJob: setSelectedJob,
updateUnclassifiedFailureMap: updateUnclassifiedFailureMap,
defaultResultSetCount: defaultResultSetCount,
diff --git a/ui/js/providers.js b/ui/js/providers.js
index 7a2f0bfa5..d3b28ae39 100644
--- a/ui/js/providers.js
+++ b/ui/js/providers.js
@@ -25,17 +25,6 @@ treeherder.provider('thResultStatusList', function () {
};
});
-treeherder.provider('thResultStatus', function () {
- this.$get = function () {
- return function (job) {
- if (job.state === "completed") {
- return job.result;
- }
- return job.state;
- };
- };
-});
-
treeherder.provider('thResultStatusObject', function () {
var getResultStatusObject = function () {
return {
@@ -52,95 +41,6 @@ treeherder.provider('thResultStatusObject', function () {
};
});
-treeherder.provider('thResultStatusInfo', function () {
- this.$get = function () {
- return function (resultState, failure_classification_id) {
- // default if there is no match, used for pending
- var resultStatusInfo = {
- btnClass: "btn-default"
- };
-
- switch (resultState) {
- case "busted":
- case "failures":
- resultStatusInfo = {
- btnClass: "btn-red",
- countText: "busted"
- };
- break;
- case "exception":
- resultStatusInfo = {
- btnClass: "btn-purple",
- countText: "exception"
- };
- break;
- case "testfailed":
- resultStatusInfo = {
- btnClass: "btn-orange",
- countText: "failed"
- };
- break;
- case "unknown":
- resultStatusInfo = {
- btnClass: "btn-yellow",
- countText: "unknown"
- };
- break;
- case "usercancel":
- resultStatusInfo = {
- btnClass: "btn-pink",
- countText: "cancel"
- };
- break;
- case "retry":
- resultStatusInfo = {
- btnClass: "btn-dkblue",
- countText: "retry"
- };
- break;
- case "success":
- resultStatusInfo = {
- btnClass: "btn-green",
- countText: "success"
- };
- break;
- case "running":
- case "in progress":
- resultStatusInfo = {
- btnClass: "btn-dkgray",
- countText: "running"
- };
- break;
- case "pending":
- resultStatusInfo = {
- btnClass: "btn-ltgray",
- countText: "pending"
- };
- break;
- case "superseded":
- resultStatusInfo = {
- btnClass: "btn-ltblue",
- countText: "superseded"
- };
- break;
- }
-
- // handle if a job is classified
- var classificationId = parseInt(failure_classification_id, 10);
- if (classificationId > 1) {
- resultStatusInfo.btnClass += "-classified";
- // autoclassification-only case
- if (classificationId === 7) {
- resultStatusInfo.btnClass += " autoclassified";
- }
- resultStatusInfo.countText = "classified " + resultStatusInfo.countText;
- }
- return resultStatusInfo;
- };
-
- };
-});
-
/**
* The set of custom Treeherder events.
*
@@ -183,6 +83,9 @@ treeherder.provider('thEvents', function () {
// after loading a group of jobs
jobsLoaded: "jobs-loaded-EVT",
+ // when new pushes are prepended, or appended
+ pushesLoaded: "pushes-loaded-EVT",
+
// after deselecting a job via click outside/esc
clearSelectedJob: "clear-selected-job-EVT",
@@ -239,8 +142,9 @@ treeherder.provider('thEvents', function () {
autoclassifyToggleExpandOptions: "ac-toggle-expand-options-EVT",
- autoclassifyToggleEdit: "ac-toggle-edit-EVT"
+ autoclassifyToggleEdit: "ac-toggle-edit-EVT",
+ selectRunnableJob: "select-runnable-job-EVT",
};
};
});
@@ -249,7 +153,7 @@ treeherder.provider('thAggregateIds', function () {
this.$get = function () {
return {
getPlatformRowId: aggregateIds.getPlatformRowId,
- getResultsetTableId: aggregateIds.getResultsetTableId,
+ getPushTableId: aggregateIds.getPushTableId,
getGroupMapKey: aggregateIds.getGroupMapKey,
escape: aggregateIds.escape
};
diff --git a/ui/js/reactrevisions.jsx b/ui/js/reactrevisions.jsx
deleted file mode 100644
index e69de29bb..000000000
diff --git a/ui/js/services/jobfilters.js b/ui/js/services/jobfilters.js
index 33eb7b1f1..1dbba7c31 100644
--- a/ui/js/services/jobfilters.js
+++ b/ui/js/services/jobfilters.js
@@ -1,3 +1,4 @@
+import { getStatus } from '../../helpers/jobHelper';
/**
This service handles whether or not a job, job group or platform row should
be displayed based on the filter settings.
@@ -18,13 +19,13 @@
*/
treeherder.factory('thJobFilters', [
'thResultStatusList', 'ThLog', '$rootScope', '$location',
- 'thEvents', 'thFailureResults',
- 'thResultStatus', 'thClassificationTypes',
+ 'thEvents', 'thFailureResults', '$timeout',
+ 'thClassificationTypes',
'thPlatformName',
function (
thResultStatusList, ThLog, $rootScope, $location,
- thEvents, thFailureResults,
- thResultStatus, thClassificationTypes,
+ thEvents, thFailureResults, $timeout,
+ thClassificationTypes,
thPlatformName) {
const $log = new ThLog("thJobFilters");
@@ -193,9 +194,10 @@ treeherder.factory('thJobFilters', [
function showJob(job) {
// when runnable jobs have been added to a resultset, they should be
// shown regardless of settings for classified or result state
- if (job.result !== "runnable") {
+ const status = getStatus(job);
+ if (status !== "runnable") {
// test against resultStatus and classifiedState
- if (cachedResultStatusFilters.indexOf(thResultStatus(job)) === -1) {
+ if (cachedResultStatusFilters.indexOf(status) === -1) {
return false;
}
if (!_checkClassifiedStateFilters(job)) {
@@ -281,7 +283,9 @@ treeherder.factory('thJobFilters', [
newQsVal = null;
}
$log.debug("add set " + _withPrefix(field) + " from " + oldQsVal + " to " + newQsVal);
- $location.search(_withPrefix(field), newQsVal);
+ $timeout(() => {
+ $location.search(_withPrefix(field), newQsVal);
+ }, 0);
}
function removeFilter(field, value) {
diff --git a/ui/js/services/log.js b/ui/js/services/log.js
index 3e1012dc7..e9f06a9ad 100644
--- a/ui/js/services/log.js
+++ b/ui/js/services/log.js
@@ -41,11 +41,11 @@ treeherder.factory('ThLog', [
/**
* You can use this to configure which debug lines you want to see in your
- * ``local.conf.js`` file. You can see ONLY ``ResultSetCtrl`` lines by adding
+ * ``local.conf.js`` file. You can see ONLY ``foo`` lines by adding
* a line like:
*
* ThLogConfigProvider.setWhitelist([
- * 'ResultSetCtrl'
+ * 'foo'
* ]);
*
* Note: even though this is called ThLogConfig, when you configure it, you must
diff --git a/ui/js/services/main.js b/ui/js/services/main.js
index 401680345..8b2dc9d95 100755
--- a/ui/js/services/main.js
+++ b/ui/js/services/main.js
@@ -198,6 +198,32 @@ treeherder.factory('Ajv', [
return require('ajv');
}]);
+// The Custom Actions modal is accessible from both the PushActionMenu and the
+// job-details-actionbar. So leave this as Angular for now, till we
+// migrate job-details-actionbar to React.
+treeherder.factory('customPushActions', [
+ '$uibModal',
+ function ($uibModal) {
+ return {
+ open(repoName, pushId) {
+ $uibModal.open({
+ templateUrl: 'partials/main/tcjobactions.html',
+ controller: 'TCJobActionsCtrl',
+ size: 'lg',
+ resolve: {
+ job: () => null,
+ repoName: function () {
+ return repoName;
+ },
+ resultsetId: function () {
+ return pushId;
+ }
+ }
+ });
+ }
+ };
+ }]);
+
treeherder.factory('jsonSchemaDefaults', [
function () {
return require('json-schema-defaults');
diff --git a/ui/js/treeherder_app.js b/ui/js/treeherder_app.js
index ca10ee269..fd995d315 100644
--- a/ui/js/treeherder_app.js
+++ b/ui/js/treeherder_app.js
@@ -32,15 +32,11 @@ treeherderApp.config(['$compileProvider', '$locationProvider', '$routeProvider',
$routeProvider
.when('/jobs', {
- controller: 'JobsCtrl',
- templateUrl: 'partials/main/jobs.html',
// see controllers/filters.js ``skipNextSearchChangeReload`` for
// why we set this to false.
reloadOnSearch: false
})
.when('/jobs/:tree', {
- controller: 'JobsCtrl',
- templateUrl: 'partials/main/jobs.html',
reloadOnSearch: false
})
.otherwise({ redirectTo: '/jobs' });
diff --git a/ui/js/values.js b/ui/js/values.js
index 9dce830d1..fd79d10c6 100644
--- a/ui/js/values.js
+++ b/ui/js/values.js
@@ -159,12 +159,11 @@ treeherder.value("thJobNavSelectors",
{
ALL_JOBS: {
name: "jobs",
- selector: ".job-btn, .selected-job, .selected-count"
+ selector: ".job-btn, .selected-job"
},
UNCLASSIFIED_FAILURES: {
name: "unclassified failures",
selector: ".selected-job, " +
- ".selected-count, " +
".job-btn.btn-red, " +
".job-btn.btn-orange, " +
".job-btn.btn-purple, " +
diff --git a/ui/partials/main/jobs.html b/ui/partials/main/jobs.html
deleted file mode 100755
index ae324ac0d..000000000
--- a/ui/partials/main/jobs.html
+++ /dev/null
@@ -1,131 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
- This action will cancel all pending and running jobs for this push.
- It cannot be undone!
-
-
-
-
-
-
-
-
-
-
-
- Waiting for a push with revision {{revision}}
- (view pushlog)
-
- If the push exists, it will appear in a few minutes once it has been processed.
-
-
- This is an invalid revision parameter. Please change it, or click
- here to reload the latest revisions from {{repoName}}.
-
-
-
-
-
-
-
- No pushes found.
- No commit information could be loaded for this repository.
- More information about this repository can be found here.
-
-
-
-
-
- Unknown repository.
- This repository is either unknown to Treeherder or it doesn't exist.
- If this repository does exist, please
-
- file a bug against the Treeherder product in Bugzilla to get it added to the system.
-
-
-
-
-
-
-
-
-
-
diff --git a/ui/partials/main/thActionButton.html b/ui/partials/main/thActionButton.html
deleted file mode 100644
index 5c60a025f..000000000
--- a/ui/partials/main/thActionButton.html
+++ /dev/null
@@ -1,50 +0,0 @@
-
-
-
-
-
diff --git a/ui/partials/main/thFilterCheckbox.html b/ui/partials/main/thFilterCheckbox.html
deleted file mode 100644
index e69de29bb..000000000
diff --git a/ui/partials/main/thPinnedJob.html b/ui/partials/main/thPinnedJob.html
index dd8d107cd..a7e92154c 100644
--- a/ui/partials/main/thPinnedJob.html
+++ b/ui/partials/main/thPinnedJob.html
@@ -1,5 +1,5 @@
-
- - Complete -
- {{percentComplete}}% - {{inProgress}} in progress
-
diff --git a/ui/plugins/controller.js b/ui/plugins/controller.js
index 0a71e7558..b955e546c 100644
--- a/ui/plugins/controller.js
+++ b/ui/plugins/controller.js
@@ -1,11 +1,12 @@
import { Queue, slugid } from 'taskcluster-client-web';
import thTaskcluster from '../js/services/taskcluster';
+import { getStatus } from '../helpers/jobHelper';
treeherder.controller('PluginCtrl', [
'$scope', '$rootScope', '$location', '$http', '$interpolate', '$uibModal',
'thUrl', 'ThJobClassificationModel',
'thClassificationTypes', 'ThJobModel', 'thEvents', 'dateFilter', 'thDateFormat',
- 'numberFilter', 'ThBugJobMapModel', 'thResultStatus', 'thJobFilters',
+ 'numberFilter', 'ThBugJobMapModel', 'thJobFilters',
'ThLog', '$q', 'thPinboard',
'ThJobDetailModel', 'thBuildApi', 'thNotify', 'ThJobLogUrlModel', 'ThModelErrors', 'ThTaskclusterErrors',
'thTabs', '$timeout', 'thReftestStatus', 'ThResultSetStore',
@@ -14,7 +15,7 @@ treeherder.controller('PluginCtrl', [
$scope, $rootScope, $location, $http, $interpolate, $uibModal,
thUrl, ThJobClassificationModel,
thClassificationTypes, ThJobModel, thEvents, dateFilter, thDateFormat,
- numberFilter, ThBugJobMapModel, thResultStatus, thJobFilters,
+ numberFilter, ThBugJobMapModel, thJobFilters,
ThLog, $q, thPinboard,
ThJobDetailModel, thBuildApi, thNotify, ThJobLogUrlModel, ThModelErrors, ThTaskclusterErrors, thTabs,
$timeout, thReftestStatus, ThResultSetStore, PhSeries,
@@ -92,7 +93,7 @@ treeherder.controller('PluginCtrl', [
successTab = 'perfDetails';
}
- if (thResultStatus(job) === 'success') {
+ if (getStatus(job) === 'success') {
$scope.tabService.selectedTab = successTab;
} else {
$scope.tabService.selectedTab = failTab;
@@ -191,7 +192,7 @@ treeherder.controller('PluginCtrl', [
if ($scope.job_log_urls.length) {
$scope.reftestUrl = reftestUrlRoot + $scope.job_log_urls[0].url + "&only_show_unexpected=1";
}
- $scope.resultStatusShading = "result-status-shading-" + thResultStatus($scope.job);
+ $scope.resultStatusShading = "result-status-shading-" + getStatus($scope.job);
var performanceData = _.flatten(Object.values(results[3]));
if (performanceData) {
diff --git a/ui/plugins/similar_jobs/controller.js b/ui/plugins/similar_jobs/controller.js
index b306e4161..d00a62491 100755
--- a/ui/plugins/similar_jobs/controller.js
+++ b/ui/plugins/similar_jobs/controller.js
@@ -1,12 +1,14 @@
+import { getBtnClass, getStatus } from "../../helpers/jobHelper";
+
treeherder.controller('SimilarJobsPluginCtrl', [
- '$scope', 'ThLog', 'ThJobModel', 'ThTextLogStepModel', 'thResultStatusInfo',
+ '$scope', 'ThLog', 'ThJobModel', 'ThTextLogStepModel',
'numberFilter', 'dateFilter', 'thClassificationTypes',
- 'thResultStatus', 'ThResultSetModel', 'thNotify',
+ 'ThResultSetModel', 'thNotify',
'thTabs',
function SimilarJobsPluginCtrl(
- $scope, ThLog, ThJobModel, ThTextLogStepModel, thResultStatusInfo,
+ $scope, ThLog, ThJobModel, ThTextLogStepModel,
numberFilter, dateFilter, thClassificationTypes,
- thResultStatus, ThResultSetModel, thNotify,
+ ThResultSetModel, thNotify,
thTabs) {
var $log = new ThLog(this.constructor.name);
@@ -85,20 +87,14 @@ treeherder.controller('SimilarJobsPluginCtrl', [
$scope.similar_jobs = [];
- $scope.result_status_info = thResultStatusInfo;
-
$scope.similar_jobs_filters = {
machine_id: false,
build_platform_id: true,
option_collection_hash: true
};
- $scope.button_class = function (job) {
- var resultState = job.result;
- if (job.state !== "completed") {
- resultState = job.state;
- }
- return thResultStatusInfo(resultState).btnClass;
- };
+ $scope.button_class = job => (
+ getBtnClass(getStatus(job))
+ );
// this is triggered by the show more link
$scope.show_next = function () {
@@ -112,7 +108,7 @@ treeherder.controller('SimilarJobsPluginCtrl', [
ThJobModel.get($scope.repoName, job.id)
.then(function (job) {
$scope.similar_job_selected = job;
- $scope.similar_job_selected.result_status = thResultStatus($scope.similar_job_selected);
+ $scope.similar_job_selected.result_status = getStatus($scope.similar_job_selected);
var duration = (
$scope.similar_job_selected.end_timestamp - $scope.similar_job_selected.start_timestamp
)/60;