Bug 1163064 - Collapse test group chunks down to counts by result and state

This branch aggregates non-failed jobs within groups and shows the counts of each result/state within.
A ``+`` precedes each count to indicate that's what it is.

A few points of interest:
* Clicking any group or count will expand or collapse the group.  This choice is
remembered as jobs are updated and filters are changed.  Page reload resets it.
* Clicking on the ``( + )`` and ``( - )`` buttons will toggle groups expanded/collapsed
globally.  This is persisted in the URL.  Clicking this button will reset any group
expand/collapse state that was set individually.
* Failed Unclassified jobs are never put into "counts".  They show as top-level
jobs and are still hit with the ``n`` and ``p`` hot keys, etc.

Also fixes:
* Bug 1191454 - classifying 3 jobs, one selected, then clicking "n" for next will go to the first
* Bug 1191487 - Re-scroll selected job into view during chunk expand/collapse

This reverts commit 2e6b7b56c6.
This commit is contained in:
Cameron Dawson 2015-08-13 16:34:13 -07:00
Родитель c0c553e62d
Коммит e633a67640
10 изменённых файлов: 495 добавлений и 154 удалений

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

@ -594,6 +594,11 @@ th-watched-repo {
display: block;
}
.group-state-nav-icon {
width: 7px;
display: inline-block;
}
.job-group {
margin: 0 -3px 0 3px;
}
@ -606,11 +611,52 @@ th-watched-repo {
display: none;
}
.group-btn {
background: transparent;
padding: 0 2px 0 2px;
vertical-align: 0;
line-height: 1.32;
cursor: pointer;
}
.group-btn::before {
content: "+";
}
.group-symbol:hover {
background-color: rgba(208, 228, 250, 0.51);
cursor: pointer;
}
.group-content {
margin-left: -3px;
cursor: pointer;
}
.group-content::before {
content: "("
}
.group-content::after {
content: ")"
}
.group-count-list:hover {
background-color: rgba(208, 228, 250, 0.51);
}
.group-job-list {
margin-left: -3px;
}
.selected-job {
border: 4px solid;
background-color: #fff;
}
.selected-count.btn-lg-xform {
background-color: #fff;
}
.filter-shown {
display: inline-block;
}
@ -1255,7 +1301,14 @@ ul.failure-summary-list li .btn-xs {
.job-btn.btn-ltgray,
.job-btn.btn-green,
.job-btn.btn-dkblue,
.job-btn.btn-pink {
.job-btn.btn-yellow,
.job-btn.btn-pink,
.group-btn.btn-dkgray-count,
.group-btn.btn-ltgray-count,
.group-btn.btn-green-count,
.group-btn.btn-dkblue-count,
.group-btn.btn-yellow-count,
.group-btn.btn-pink-count {
margin: 0 -5px 0 -1px;
}
@ -1265,6 +1318,29 @@ ul.failure-summary-list li .btn-xs {
margin: 0 -3px 1px 0;
}
.btn-orange-classified::after,
.btn-orange-classified-count::after,
.btn-red-classified::after,
.btn-red-classified-count::after,
.btn-black-classified::after,
.btn-black-classified-count::after,
.btn-green-classified::after,
.btn-green-classified-count::after,
.btn-dkblue-classified::after,
.btn-dkblue-classified-count::after,
.btn-dkgray-classified::after,
.btn-dkgray-classified-count::after,
.btn-ltgray-classified::after,
.btn-ltgray-classified-count::after,
.btn-yellow-classified::after,
.btn-yellow-classified-count::after,
.btn-pink-classified::after,
.btn-pink-classified-count::after,
.btn-purple-classified::after,
.btn-purple-classified-count::after {
content: "*";
}
.btn-view-nav {
background-color: transparent;
border-color: #373d40;
@ -1402,7 +1478,8 @@ fieldset[disabled] .btn-orange.active {
border-color: #dd6602;
}
.btn-orange-classified {
.btn-orange-classified,
.btn-orange-classified-count {
color: #dd6602;
}
.btn-orange-classified:hover,
@ -1459,7 +1536,8 @@ fieldset[disabled] .btn-red.active {
border-color: #c2020e;
}
.btn-red-classified {
.btn-red-classified,
.btn-red-classified-count {
color: #90010a;
}
.btn-red-classified:hover,
@ -1487,7 +1565,10 @@ fieldset[disabled] .btn-red-classified.active {
color: white;
}
.btn-dkblue {
.btn-dkblue,
.btn-dkblue-count,
.btn-dkblue-classified,
.btn-dkblue-classified-count {
color: #283aa2;
font-weight: bold;
}
@ -1516,7 +1597,10 @@ fieldset[disabled] .btn-dkblue.active {
border-color: #2d48d6;
}
.btn-green {
.btn-green,
.btn-green-count,
.btn-green-classified,
.btn-green-classified-count {
color: rgba(2, 130, 51, 0.75);
font-weight: bold;
}
@ -1543,7 +1627,8 @@ fieldset[disabled] .btn-green.active {
border-color: #02c238;
}
.btn-purple {
.btn-purple
.btn-purple-count {
background-color: #9a7da6;
border-color: #6f0296;
color: white;
@ -1573,7 +1658,8 @@ fieldset[disabled] .btn-purple.active {
color: white;
}
.btn-purple-classified {
.btn-purple-classified,
.btn-purple-classified-count {
color: #6f0296;
}
.btn-purple-classified:hover,
@ -1601,7 +1687,10 @@ fieldset[disabled] .btn-purple-classified.active {
color: white;
}
.btn-yellow {
.btn-yellow,
.btn-yellow-count,
.btn-yellow-classified,
.btn-yellow-classified-count {
color: #cdce1d;
font-weight: bold;
}
@ -1627,7 +1716,10 @@ fieldset[disabled] .btn-yellow.active {
border-color: #cdce1d;
}
.btn-ltgray {
.btn-ltgray,
.btn-ltgray-count,
.btn-ltgray-classified,
.btn-ltgray-classified-count {
color: #e0e0e0;
}
.btn-ltgray:hover,
@ -1653,7 +1745,11 @@ fieldset[disabled] .btn-ltgray.active {
border-color: #e0e0e0;
}
.btn-mdgray {
.btn-mdgray,
.btn-mdgray-count,
.btn-mdgray-classified,
.btn-mdgray-classified-count
{
background-color: #bfbfbf;
border-color: #bfbfbf;
}
@ -1700,7 +1796,10 @@ fieldset[disabled] .btn-resultset:hover {
color: white;
}
.btn-dkgray {
.btn-dkgray,
.btn-dkgray-count,
.btn-dkgray-classified,
.btn-dkgray-classified-count {
color: #7c7a7d;
}
.btn-dkgray:hover,
@ -1728,7 +1827,10 @@ fieldset[disabled] .btn-dkgray.active {
color: white;
}
.btn-black {
.btn-black
.btn-black-count,
.btn-black-classified,
.btn-black-classified-count {
background-color: #4a4a4a;
border-color: #000000;
color: white;
@ -1781,7 +1883,10 @@ fieldset[disabled] .btn-black.active {
border-color: #000000;
}
.btn-pink {
.btn-pink
.btn-pink-count,
.btn-pink-classified,
.btn-pink-classified-count {
color: #ff40d9;
font-weight: bold;
}

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

@ -171,13 +171,26 @@
</script>
<!-- Start span for job groups -->
<script type="'text/ng-template'" id="jobGroupBeginClone.html">
<script type="'text/ng-template'" id="jobGroupClone.html">
<span class="platform-group">
<span class="disabled job-group" title="{{ name }}">{{ symbol }}(</span>
<span class="job-group-list"></span>)
<span class="disabled job-group" title="{{ name }}"
data-grkey="{{ grkey }}">
<span class="group-symbol"
ignore-job-clear-on-click>{{ symbol }}</span>
<span class="group-content">
<span class="group-job-list"></span>
<span class="group-count-list"></span>
</span>
</span>
</span>
</script>
<!-- Job group count span for each count item -->
<script type="'text/ng-template'" id="jobGroupCountClone.html">
<button class="btn {{ selectedClasses }} group-btn btn-xs job-group-count {{ btnClass }}"
title="{{ title }}">{{ value }}</button>
</script>
<!-- Job Btn span -->
<script type="'text/ng-template'" id="jobBtnClone.html">
<button class="btn job-btn btn-xs {{ btnClass }} {{ key }}"

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

@ -319,6 +319,17 @@ treeherderApp.controller('MainCtrl', [
};
$scope.getGroupState = function() {
return $location.search().group_state || "collapsed";
};
$scope.groupState = $scope.getGroupState();
$scope.toggleGroupState = function() {
var newGroupState = $scope.groupState === "collapsed" ? "expanded" : null;
$location.search("group_state", newGroupState);
};
var getNewReloadTriggerParams = function() {
return _.pick(
$location.search(),
@ -364,6 +375,13 @@ treeherderApp.controller('MainCtrl', [
}
$rootScope.skipNextPageReload = false;
// handle a change in the groupState whether it was by the button
// or directly in the url.
var newGroupState = $scope.getGroupState();
if (newGroupState !== $scope.groupState) {
$scope.groupState = newGroupState;
$rootScope.$emit(thEvents.groupStateChanged);
}
});
$scope.changeRepo = function(repo_name) {

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

@ -23,6 +23,7 @@ treeherder.directive('thCloneJobs', [
// CSS classes
var btnCls = 'btn-xs';
var selectedBtnCls = 'selected-job';
var selectedCountCls = 'selected-count';
var largeBtnCls = 'btn-lg-xform';
var col5Cls = 'col-xs-5';
@ -31,8 +32,13 @@ treeherder.directive('thCloneJobs', [
var jobListNoPadCls = 'job-list-nopad';
var jobListPadCls = 'job-list-pad';
var viewContentSel = ".th-view-content";
var failResults = ["testfailed", "busted", "exception"];
// Custom Attributes
var jobKeyAttr = 'data-jmkey';
var groupKeyAttr = 'data-grkey';
var tableInterpolator = thCloneHtml.get('resultsetClone').interpolator;
@ -40,7 +46,10 @@ treeherder.directive('thCloneJobs', [
var platformInterpolator = thCloneHtml.get('platformClone').interpolator;
//Instantiate job group interpolator
var jobGroupInterpolator = thCloneHtml.get('jobGroupBeginClone').interpolator;
var jobGroupInterpolator = thCloneHtml.get('jobGroupClone').interpolator;
//Instantiate job group count interpolator
var jobGroupCountInterpolator = thCloneHtml.get('jobGroupCountClone').interpolator;
//Instantiate job btn interpolator
var jobBtnInterpolator = thCloneHtml.get('jobBtnClone').interpolator;
@ -85,9 +94,9 @@ treeherder.directive('thCloneJobs', [
// 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.
jobs = $(".th-view-content").find(jobNavSelector.selector).filter(":visible, .selected-job");
jobs = $(viewContentSel).find(jobNavSelector.selector).filter(":visible, .selected-job, .selected-count");
if (jobs.length) {
var selIdx = jobs.index(jobs.filter(".selected-job"));
var selIdx = jobs.index(jobs.filter(".selected-job, .selected-count").first());
var idx = getIndex(selIdx, jobs);
el = $(jobs[idx]);
@ -126,20 +135,12 @@ treeherder.directive('thCloneJobs', [
};
var setSelectJobStyles = function(el){
var lastJobSelected = ThResultSetStore.getSelectedJob(
$rootScope.repoName);
if(!_.isEmpty(lastJobSelected.el)){
lastJobSelected.el.removeClass(selectedBtnCls);
lastJobSelected.el.removeClass(largeBtnCls);
lastJobSelected.el.addClass(btnCls);
}
// clear the styles from the previously selected job, if any.
clearSelectJobStyles();
el.removeClass(btnCls);
el.addClass(largeBtnCls);
el.addClass(selectedBtnCls);
};
var clearSelectJobStyles = function() {
@ -151,6 +152,16 @@ treeherder.directive('thCloneJobs', [
lastJobSelected.el.removeClass(largeBtnCls);
lastJobSelected.el.addClass(btnCls);
}
// if a job was previously selected that is now inside a count,
// then the count will have the ``.selected-count`` class. Since
// we are now selecting a job, we need to remove that class from the
// count.
var selectedCount = $(viewContentSel).find("."+selectedCountCls);
if (selectedCount.length) {
selectedCount.removeClass(selectedCountCls);
selectedCount.removeClass(largeBtnCls);
}
};
var broadcastJobChangedTimeout = null;
@ -166,87 +177,180 @@ treeherder.directive('thCloneJobs', [
}, 200);
};
/**
* Clicking a group will expand or collapse it. Expanded shows all
* jobs. Collapsed shows counts and failed jobs.
*/
var clickGroupCb = function(el) {
var groupMap = ThResultSetStore.getGroupMap($rootScope.repoName);
var gi = getGroupInfo(el, groupMap);
if (gi) {
if (isGroupExpanded(gi.jgObj)) {
gi.jgObj.groupState = "collapsed";
addGroupJobsAndCounts(gi.jgObj, gi.platformGroupEl);
} else {
gi.grpCountList.empty();
gi.jgObj.groupState = "expanded";
addJobBtnEls(gi.jgObj, gi.grpJobList);
}
}
};
var togglePinJobCb = function(ev, el, job){
$rootScope.$emit(thEvents.jobPin, job);
};
var addJobBtnEls = function(
jgObj, jobBtnInterpolator, jobTdEl){
var jobsShown = 0;
var lastJobSelected = ThResultSetStore.getSelectedJob(
$rootScope.repoName
);
var hText, key, resultState, job, jobStatus, jobBtn, l;
var addJobBtnEls = function(jgObj, jobList) {
var lastJobSelected = ThResultSetStore.getSelectedJob($rootScope.repoName);
var job, l;
var jobBtnArray = [];
jobList.empty();
for(l=0; l<jgObj.jobs.length; l++){
job = jgObj.jobs[l];
//Set the resultState
resultState = thResultStatus(job);
job.searchStr = thJobSearchStr(job) + ' ' + job.ref_data_name + ' ' +
job.signature;
//Make sure that filtering doesn't effect the resultset counts
//displayed
if(thJobFilters.showJob(job) === false){
//Keep track of visibility with this property. This
//way down stream job consumers don't need to repeatedly
//call showJob
job.visible = false;
// Keep track of visibility with this property. This
// way down stream job consumers don't need to repeatedly
// call showJob
job.visible = thJobFilters.showJob(job);
addJobBtnToArray(job, lastJobSelected, jobBtnArray);
}
jobList.append(jobBtnArray);
};
var addJobBtnToArray = function(job, lastJobSelected, jobBtnArray) {
var jobStatus, jobBtn;
jobStatus = thResultStatusInfo(thResultStatus(job), job.failure_classification_id);
jobStatus.key = getJobMapKey(job);
jobStatus.value = job.job_type_symbol;
jobStatus.title = getHoverText(job);
jobBtn = $(jobBtnInterpolator(jobStatus));
// If the job is currently selected make sure to re-apply
// the job selection styles
if( !_.isEmpty(lastJobSelected.job) &&
(lastJobSelected.job.id === job.id)){
setSelectJobStyles(jobBtn);
//Update the selected job element to the current one
ThResultSetStore.setSelectedJob($rootScope.repoName, jobBtn, job);
}
showHideElement(jobBtn, job.visible);
jobBtnArray.push(jobBtn);
// add a zero-width space between spans so they can wrap
jobBtnArray.push(' ');
};
var getGroupInfo = function(el, groupMap) {
var gi = {};
try {
gi.platformGroupEl = $(el).closest(".platform-group");
gi.grpJobList = gi.platformGroupEl.find(".group-job-list");
gi.grpCountList = gi.platformGroupEl.find(".group-count-list");
gi.key = gi.platformGroupEl.find(".job-group").attr(groupKeyAttr);
gi.jgObj = groupMap[gi.key].grp_obj;
return gi;
} catch(TypeError) {
return null;
}
};
/**
* Group non-failed jobs as '+n' counts in the UI by default,
* and failed jobs as individual buttons.
* Each job receives a corresponding resultState which determines its
* display.
*/
var addGroupJobsAndCounts = function(jgObj, platformGroup) {
var ct, job, jobCountBtn, l;
var countAdded = false;
var jobCountBtnArray = [];
var jobBtnArray = [];
var stateCounts = {};
var lastJobSelected = ThResultSetStore.getSelectedJob($rootScope.repoName);
var jobList = platformGroup.find(".group-job-list");
var countList = platformGroup.find(".group-count-list");
jobList.empty();
countList.empty();
for (l = 0; l < jgObj.jobs.length; l++) {
job = jgObj.jobs[l];
job.searchStr = thJobSearchStr(job) + ' ' + job.ref_data_name + ' ' +
job.signature;
//Set the resultState
var resultStatus = thResultStatus(job);
var countInfo = thResultStatusInfo(resultStatus,
job.failure_classification_id);
job.visible = thJobFilters.showJob(job);
// Even if a job is not visible, add it to the DOM as hidden. This is
// important because it can still be "selected" when not visible
// or filtered out (like in the case of unclassified failures).
//
// We don't add it to group counts, because it should not be counted
// when filtered out. Failures don't get included in counts anyway.
if (_.contains(failResults, resultStatus)) {
// render the job itself, not a count
addJobBtnToArray(job, lastJobSelected, jobBtnArray);
} else {
jobsShown++;
job.visible = true;
}
if (job.visible) {
_.extend(countInfo, stateCounts[countInfo.btnClass]);
if (!_.isEmpty(lastJobSelected.job) &&
(lastJobSelected.job.id === job.id)) {
// these classes are applied in the interpolator
// to designate this count as having one of its
// jobs selected.
countInfo.selectedClasses = selectedCountCls + " " + largeBtnCls;
}
hText = getHoverText(job);
key = getJobMapKey(job);
jobStatus = thResultStatusInfo(resultState);
//Add a visual indicator for a failure classification
jobStatus.key = key;
if(parseInt(job.failure_classification_id, 10) > 1){
jobStatus.value = job.job_type_symbol + '*';
if (jobStatus.btnClassClassified) {
// For result types that are displayed more prominently
// when unclassified, switch to the more subtle classified
// style.
jobStatus.btnClass = jobStatus.btnClassClassified;
ct = _.get(_.get(stateCounts, countInfo.btnClass, countInfo),
"count", 0);
countInfo.count = ct + 1;
// keep a reference to the job. If there ends up being
// only one for this status, then just add the job itself
// rather than a count.
countInfo.lastJob = job;
stateCounts[countInfo.btnClass] = countInfo;
}
} else {
jobStatus.value = job.job_type_symbol;
}
jobStatus.title = hText;
jobBtn = $( jobBtnInterpolator(jobStatus));
jobBtnArray.push(jobBtn);
// add a zero-width space between spans so they can wrap
jobBtnArray.push(' ');
showHideJob(jobBtn, job.visible);
//If the job is currently selected make sure to re-apply
//the job selection styles
if( !_.isEmpty(lastJobSelected.job) &&
(lastJobSelected.job.id === job.id)){
setSelectJobStyles(jobBtn);
//Update the selected job element to the current one
ThResultSetStore.setSelectedJob(
$rootScope.repoName, jobBtn, job);
}
}
jobTdEl.append(jobBtnArray);
return jobsShown;
_.forEach(stateCounts, function(countInfo) {
if (countInfo.count === 1) {
// if there is only 1 job for this status, then just add
// the job, rather than the count
addJobBtnToArray(countInfo.lastJob, lastJobSelected, jobBtnArray);
} else {
// with more than 1 job for the status, add it as a count
countAdded = true;
countInfo.value = countInfo.count;
countInfo.title = countInfo.count + " " + countInfo.countText + " jobs in group";
countInfo.btnClass = countInfo.btnClass + "-count";
jobCountBtn = $(jobGroupCountInterpolator(countInfo));
jobCountBtnArray.push(jobCountBtn);
jobCountBtnArray.push(' ');
showHideElement(jobCountBtn, true);
}
});
jobList.append(jobBtnArray);
if (countAdded) {
countList.append(jobCountBtnArray);
}
};
var jobMouseDown = function(resultset, ev){
@ -292,6 +396,8 @@ treeherder.directive('thCloneJobs', [
ThResultSetStore.setSelectedJob($rootScope.repoName, el, job);
} else {
_.bind(clickGroupCb, this, el)();
}
};
@ -308,7 +414,7 @@ treeherder.directive('thCloneJobs', [
var revision, revisionHtml, userTokens, i;
for(i=0; i<resultset.revisions.length; i++){
for (i=0; i<resultset.revisions.length; i++) {
revision = resultset.revisions[i];
@ -337,7 +443,7 @@ treeherder.directive('thCloneJobs', [
}
};
var toggleRevisions = function(element, expand){
var toggleRevisions = function(element, expand) {
var revisionsEl = element.find('ul').parent();
var jobsEl = element.find('table').parent();
@ -353,13 +459,13 @@ treeherder.directive('thCloneJobs', [
var rowEl = revisionsEl.parent();
rowEl.css('display', 'block');
if(on) {
if (on) {
ThResultSetStore.loadRevisions(
$rootScope.repoName, this.resultset.id
);
if(jobsElDisplayState === 'block'){
if (jobsElDisplayState === 'block') {
toggleRevisionsSpanOnWithJobs(revisionsEl);
//Make sure the jobs span has correct styles
toggleJobsSpanOnWithRevisions(jobsEl);
@ -372,22 +478,22 @@ treeherder.directive('thCloneJobs', [
};
var toggleRevisionsSpanOnWithJobs = function(el){
var toggleRevisionsSpanOnWithJobs = function(el) {
el.css('display', 'block');
el.addClass(col5Cls);
};
var toggleRevisionsSpanOff = function(el){
var toggleRevisionsSpanOff = function(el) {
el.css('display', 'none');
el.removeClass(col5Cls);
};
var toggleJobsSpanOnWithRevisions = function(el){
var toggleJobsSpanOnWithRevisions = function(el) {
el.css('display', 'block');
el.removeClass(jobListNoPadCls);
el.removeClass(col12Cls);
el.addClass(col7Cls);
el.addClass(jobListPadCls);
};
var toggleJobsSpanOnWithoutRevisions = function(el){
var toggleJobsSpanOnWithoutRevisions = function(el) {
el.css('display', 'block');
el.removeClass(col7Cls);
el.removeClass(jobListPadCls);
@ -400,29 +506,26 @@ treeherder.directive('thCloneJobs', [
//Empty the job column before populating it
jobTdEl.empty();
var jgObj, jobGroup, jobsShown, i;
for(i=0; i<jobGroups.length; i++){
var jgObj, jobGroup, i;
for (i=0; i<jobGroups.length; i++) {
jgObj = jobGroups[i];
jobsShown = 0;
if(jgObj.symbol !== '?'){
if (jgObj.symbol !== '?') {
// Job group detected, add job group symbols
jobGroup = $( jobGroupInterpolator(jobGroups[i]) );
jobGroups[i].grkey = jgObj.mapKey;
jobGroups[i].collapsed = true;
jobGroup = $(jobGroupInterpolator(jobGroups[i]));
jobTdEl.append(jobGroup);
if (isGroupExpanded(jgObj)) {
addJobBtnEls(jgObj, jobGroup.find(".group-job-list"));
} else {
addGroupJobsAndCounts(jgObj, jobGroup);
}
} else {
// Add the job btn spans
jobsShown = addJobBtnEls(
jgObj, jobBtnInterpolator, jobGroup.find(".job-group-list"));
jobGroup.css("display", jobsShown? "inline": "none");
}else{
// Add the job btn spans
jobsShown = addJobBtnEls(
jgObj, jobBtnInterpolator, jobTdEl);
addJobBtnEls(jgObj, jobTdEl);
}
}
row.append(jobTdEl);
@ -446,10 +549,11 @@ treeherder.directive('thCloneJobs', [
job = jobMap[jmKey].job_obj;
show = thJobFilters.showJob(job);
job.visible = show;
showHideJob($(this), show);
showHideElement($(this), show);
});
renderGroups(element, false);
// hide platforms and groups where all jobs are hidden
element.find(".platform").each(function internalFilterPlatform() {
var platform = $(this.parentNode);
@ -457,7 +561,52 @@ treeherder.directive('thCloneJobs', [
});
};
var showHideJob = function(job, show) {
var isGroupExpanded = function(group) {
var singleGroupState = group.groupState || $scope.groupState;
return singleGroupState === "expanded";
};
/**
* Render all the job groups for a resultset. Make decisions on whether
* to render all the jobs in the group, or to collapse them as counts.
*
* If ``resetGroupState`` is set to true, then clear the ``groupState``
* for each group that may have been set when a user clicked on it.
* If false, then honor the choice to expand or collapse an individual
* group and ignore the global setting.
*
* @param element The resultset for which to render the groups.
* @param resetGroupState Whether to reset groups individual expanded
* or collapsed states.
*/
var renderGroups = function(element, resetGroupState) {
var groupMap = ThResultSetStore.getGroupMap($rootScope.repoName);
// with items in the group, it's not as simple as just hiding or
// showing a job or count. Since there can be lots of criteria for whether to show
// or hide a job, and any job hidden or shown will change the counts,
// the counts must be re-created each time.
element.find(".group-job-list").each(function internalFilterGroup(idx, el) {
var gi = getGroupInfo(el, groupMap);
gi.grpJobList.empty();
gi.grpCountList.empty();
if (resetGroupState) {
delete gi.jgObj.groupState;
}
if (isGroupExpanded(gi.jgObj)) {
addJobBtnEls(gi.jgObj, gi.platformGroupEl.find(".group-job-list"));
} else {
addGroupJobsAndCounts(gi.jgObj, gi.platformGroupEl);
}
});
};
/**
* Can be used to show/hide a job or a count of jobs
*/
var showHideElement = function(el, show) {
// Note: I was using
// jobEl.style.display = "inline";
// jobEl.className += " filter-shown";
@ -468,9 +617,9 @@ treeherder.directive('thCloneJobs', [
//
// It would be great to be able to do this without adding/removing a class
if (show) {
job[0].classList.add("filter-shown");
el[0].classList.add("filter-shown");
} else {
job[0].classList.remove("filter-shown");
el[0].classList.remove("filter-shown");
}
};
@ -482,7 +631,7 @@ treeherder.directive('thCloneJobs', [
platform[0].style.display ="table-row";
platform.find(".platform-group").each(function internalFilterGroup() {
var grp = $(this);
showGrp = grp.find('.job-group-list .filter-shown').length !== 0;
showGrp = grp.find('.group-job-list .filter-shown, .group-count-list .filter-shown').length !== 0;
grp[0].style.display = showGrp ? "inline" : "none";
});
@ -491,7 +640,7 @@ treeherder.directive('thCloneJobs', [
}
};
var appendPlatformRow = function(tableEl, rowEl, platformName){
var appendPlatformRow = function(tableEl, rowEl, platformName) {
var tableRows = $(tableEl).find('tr');
@ -529,7 +678,7 @@ treeherder.directive('thCloneJobs', [
};
var updateJobs = function(platformData){
angular.forEach(platformData, function(value, platformId){
angular.forEach(platformData, function(value, platformId) {
if(value.resultsetId !== this.resultset.id){
//Confirm we are the correct result set
@ -558,9 +707,9 @@ treeherder.directive('thCloneJobs', [
option = value.platformOption;
//Add platforms
platformTdEl = $( platformInterpolator(
{'name':platformName, 'option':option, 'id':platformId }
) );
platformTdEl = $(platformInterpolator(
{'name':platformName, 'option':option, 'id':platformId}
));
rowEl.append(platformTdEl);
@ -581,10 +730,12 @@ treeherder.directive('thCloneJobs', [
}, this);
};
var scrollToElement = function(el){
if(el.position() !== undefined){
$('.th-global-content').scrollTo(el, 100, {offset: -40});
var scrollToElement = function(el, duration) {
if (_.isUndefined(duration)) {
duration = 50;
}
if (el.position() !== undefined) {
$('.th-global-content').scrollTo(el, duration, {offset: -40});
}
};
@ -618,6 +769,12 @@ treeherder.directive('thCloneJobs', [
_.bind(filterJobs, scope, element)();
});
$rootScope.$on(
thEvents.groupStateChanged, function(ev, filterData){
_.bind(renderGroups, scope, element, true)();
scrollToElement($(viewContentSel).find(".selected-job, .selected-count"), 1);
});
$rootScope.$on(
thEvents.searchPage, function(ev, searchData){
_.bind(filterJobs, scope, element)();
@ -637,7 +794,7 @@ treeherder.directive('thCloneJobs', [
for(jid in pinnedJobs.jobs){
if (pinnedJobs.jobs.hasOwnProperty(jid)) {
//Only update the target resultset id
if(pinnedJobs.jobs[jid].result_set_id === scope.resultset.id){
if (pinnedJobs.jobs[jid].result_set_id === scope.resultset.id) {
ThResultSetStore.aggregateJobPlatform(
$rootScope.repoName, pinnedJobs.jobs[jid], platformData
);
@ -674,8 +831,7 @@ treeherder.directive('thCloneJobs', [
});
};
var generateJobElements = function(
resultsetAggregateId, resultset){
var generateJobElements = function(resultsetAggregateId, resultset) {
var tableEl = $('#' + resultsetAggregateId);
@ -683,7 +839,7 @@ treeherder.directive('thCloneJobs', [
$(waitSpanEl).css('display', 'none');
var name, option, platformId, platformKey, row, platformTd, jobTdEl, j;
for(j=0; j<resultset.platforms.length; j++){
for (j=0; j<resultset.platforms.length; j++) {
platformId = thAggregateIds.getPlatformRowId(
$rootScope.repoName,
@ -734,7 +890,10 @@ treeherder.directive('thCloneJobs', [
}
};
var linker = function(scope, element, attrs){
var $scope = null;
var linker = function(scope, element, attrs) {
$scope = scope;
//Remove any jquery on() bindings
element.off();
@ -757,10 +916,10 @@ treeherder.directive('thCloneJobs', [
element.append(targetEl);
if(scope.resultset.platforms !== undefined){
if (scope.resultset.platforms !== undefined) {
generateJobElements(
resultsetAggregateId, scope.resultset);
}else{
} else {
// Hide the job wait span, resultset has no jobs
var tableEl = $('#' + resultsetAggregateId);
var waitSpanEl = $(tableEl).prev();

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

@ -217,6 +217,7 @@ treeherder.factory('ThResultSetStore', [
// maps to help finding objects to update/add
rsMap:{},
jobMap:{},
grpMap:{},
unclassifiedFailureMap: {},
//used as the offset in paging
rsMapOldestTimestamp:null,
@ -255,11 +256,6 @@ treeherder.factory('ThResultSetStore', [
return shownJobs;
};
var getJobMapKey = function(job) {
//Build string key for jobMap entires
return 'key' + job.id;
};
var getSelectedJob = function(repoName){
return { el:repositories[repoName].lastJobElSelected,
job:repositories[repoName].lastJobObjSelected };
@ -330,6 +326,7 @@ treeherder.factory('ThResultSetStore', [
// groups
for (var gp_i = 0; gp_i < pl_obj.groups.length; gp_i++) {
var gr_obj = pl_obj.groups[gp_i];
gr_obj.mapKey = thAggregateIds.getGroupMapKey(rs_obj.id, gr_obj.name, gr_obj.symbol, pl_obj.name, pl_obj.option);
var grMapElement = {
grp_obj: gr_obj,
@ -338,10 +335,21 @@ treeherder.factory('ThResultSetStore', [
};
plMapElement.groups[gr_obj.name] = grMapElement;
// check if we need to copy groupState from an existing group
// object. This would be set if a user explicitly clicked
// a group to toggle it expanded/collapsed.
// This value will have been overwritten by the _.extend
// in mapResultSetJobs.
var oldGroup = repositories[repoName].grpMap[gr_obj.mapKey];
if (oldGroup) {
gr_obj.groupState = oldGroup.grp_obj.groupState;
}
repositories[repoName].grpMap[gr_obj.mapKey] = grMapElement;
// jobs
for (var j_i = 0; j_i < gr_obj.jobs.length; j_i++) {
var job_obj = gr_obj.jobs[j_i];
var key = getJobMapKey(job_obj);
var key = thAggregateIds.getJobMapKey(job_obj);
var jobMapElement = {
job_obj: job_obj,
@ -442,6 +450,7 @@ treeherder.factory('ThResultSetStore', [
var grp_obj = {
symbol: groupInfo.symbol,
name: groupInfo.name,
mapKey: groupInfo.mapKey,
jobs: []
};
@ -620,7 +629,7 @@ treeherder.factory('ThResultSetStore', [
*/
var updateJob = function(repoName, newJob) {
var key = getJobMapKey(newJob);
var key = thAggregateIds.getJobMapKey(newJob);
var loadedJobMap = repositories[repoName].jobMap[key];
var loadedJob = loadedJobMap? loadedJobMap.job_obj: null;
var rsMapElement = repositories[repoName].rsMap[newJob.result_set_id];
@ -785,6 +794,9 @@ treeherder.factory('ThResultSetStore', [
// 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;
};
@ -896,6 +908,8 @@ treeherder.factory('ThResultSetStore', [
var name = job.job_group_name;
var symbol = job.job_group_symbol;
var mapKey = thAggregateIds.getGroupMapKey(job.result_set_id, name, job.platform, job.platform_option);
if (job.tier && job.tier !== 1) {
if (symbol === "?") {
symbol = "";
@ -905,7 +919,7 @@ treeherder.factory('ThResultSetStore', [
symbol = tierLabel;
}
return {name: name, symbol: symbol};
return {name: name, symbol: symbol, mapKey: mapKey};
};
/*
@ -1034,6 +1048,7 @@ treeherder.factory('ThResultSetStore', [
fetchResultSets: fetchResultSets,
getAllShownJobs: getAllShownJobs,
getJobMap: getJobMap,
getGroupMap: getGroupMap,
getLoadingStatus: getLoadingStatus,
getPlatformKey: getPlatformKey,
getResultSet: getResultSet,

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

@ -64,7 +64,7 @@ treeherder.provider('thResultStatusObject', function() {
treeherder.provider('thResultStatusInfo', function() {
this.$get = function() {
return function(resultState) {
return function(resultState, failure_classification_id) {
// default if there is no match, used for pending
var resultStatusInfo = {
severity: 100,
@ -77,7 +77,6 @@ treeherder.provider('thResultStatusInfo', function() {
resultStatusInfo = {
severity: 1,
btnClass: "btn-red",
btnClassClassified: "btn-red-classified",
jobButtonIcon: "glyphicon glyphicon-fire",
countText: "busted"
};
@ -86,7 +85,6 @@ treeherder.provider('thResultStatusInfo', function() {
resultStatusInfo = {
severity: 2,
btnClass: "btn-purple",
btnClassClassified: "btn-purple-classified",
jobButtonIcon: "glyphicon glyphicon-fire",
countText: "exception"
};
@ -95,7 +93,6 @@ treeherder.provider('thResultStatusInfo', function() {
resultStatusInfo = {
severity: 3,
btnClass: "btn-orange",
btnClassClassified: "btn-orange-classified",
jobButtonIcon: "glyphicon glyphicon-warning-sign",
countText: "failed"
};
@ -104,7 +101,6 @@ treeherder.provider('thResultStatusInfo', function() {
resultStatusInfo = {
severity: 4,
btnClass: "btn-black",
btnClassClassified: "btn-black-classified",
jobButtonIcon: "",
countText: "unknown"
};
@ -159,6 +155,11 @@ treeherder.provider('thResultStatusInfo', function() {
break;
}
// handle if a job is classified
if(parseInt(failure_classification_id, 10) > 1){
resultStatusInfo.btnClass = resultStatusInfo.btnClass + "-classified";
resultStatusInfo.countText = "classified " + resultStatusInfo.countText;
}
return resultStatusInfo;
};
@ -207,6 +208,8 @@ treeherder.provider('thEvents', function() {
// fired when a global filter has changed
globalFilterChanged: "status-filter-changed-EVT",
groupStateChanged: "group-state-changed-EVT",
toggleRevisions: "toggle-revisions-EVT",
toggleAllRevisions: "toggle-all-revisions-EVT",
@ -257,10 +260,23 @@ treeherder.provider('thAggregateIds', function() {
return escape(repoName + resultsetId + revision);
};
var getGroupMapKey = function(result_set_id, grName, grSymbol, plName, plOpt) {
//Build string key for groupMap entires
return escape(result_set_id + grName + grSymbol + plName + plOpt);
};
var getJobMapKey = function(job) {
//Build string key for jobMap entires
return 'key' + job.id;
};
this.$get = function() {
return {
getPlatformRowId:getPlatformRowId,
getResultsetTableId:getResultsetTableId
getResultsetTableId:getResultsetTableId,
getJobMapKey: getJobMapKey,
getGroupMapKey: getGroupMapKey,
escape: escape
};
};
});

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

@ -42,7 +42,8 @@ treeherder.factory('thCloneHtml', [
'resultsetClone.html',
'platformClone.html',
'jobTdClone.html',
'jobGroupBeginClone.html',
'jobGroupClone.html',
'jobGroupCountClone.html',
'jobBtnClone.html',
'revisionUrlClone.html',
'pushlogRevisionsClone.html'

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

@ -162,11 +162,11 @@ treeherder.value("thJobNavSelectors",
{
ALL_JOBS: {
name: "jobs",
selector: ".job-btn"
selector: ".job-btn, .selected-job, .selected-count"
},
UNCLASSIFIED_FAILURES: {
name: "unclassified failures",
selector: ".selected-job, " +
selector: ".selected-job, .selected-count, " +
".job-btn.btn-red, " +
".job-btn.btn-orange, " +
".job-btn.btn-purple"

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

@ -36,7 +36,7 @@
<span class="btn btn-sm btn-resultset"
tabindex="0" role="button"
title="Pin all visible jobs in this resultset"
title="Pin all available jobs in this resultset"
ignore-job-clear-on-click
ng-click="pinAllShownJobs()">
<span class="glyphicon glyphicon-pushpin"

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

@ -39,6 +39,20 @@
</span>
</span>
<!--Toggle Group State Button-->
<span class="btn-group">
<span class="btn btn-view-nav btn-sm btn-toggle-group-state"
tabindex="0" role="button"
ng-click="toggleGroupState()">(
<span ng-if="groupState==='collapsed'"
class="group-state-nav-icon"
title="Expand job groups">+</span>
<span ng-if="groupState!=='collapsed'"
class="group-state-nav-icon"
title="Collapse job groups">-</span>
)</span>
</span>
<!--Quick Filter Field-->
<span ng-controller="SearchCtrl" class="form-group form-inline" id="quick-filter-parent">
<input id="quick-filter"