Redesign failed tests panel

This redesigns the failed tests panel, replacing the panel/accordion
group/table combination with a flat table that should be much easier
to extend. This also adds a new 'nest' directive to better support
dynamic hiding/showing of content. The directive is able to insert
nested content as a sibling of any normal data row, allowing for
more consistent layouts.

Change-Id: Ic0ad498049ca0f008fbcecb1dbbac3c48ab9cb41
This commit is contained in:
Tim Buckley 2016-05-17 18:20:47 -06:00
parent 75f5954a9e
commit a10d099d23
6 changed files with 256 additions and 52 deletions

View File

@ -150,7 +150,7 @@ function HomeController(
if (vm.recentRuns[link].bugs.length === 0) {
vm.recentRuns[link].bugs = '';
} else {
vm.recentRuns[link].bugs = 'Likely bugs: ' + vm.recentRuns[link].bugs.join();
vm.recentRuns[link].bugs = vm.recentRuns[link].bugs.join();
}
}
});

178
app/js/directives/nest.js Normal file
View File

@ -0,0 +1,178 @@
'use strict';
var directivesModule = require('./_index.js');
function maxColspan(element) {
var thead = element.find('thead');
var max = 0;
angular.forEach(element.find('tr'), function(row) {
var count = angular.element(row).find('th').length;
if (count > max) {
max = count;
}
});
return max;
}
/**
* @ngInject
*/
function nest($compile) {
/**
* @ngInject
*/
function controller($scope) {
var self = this;
self.open = false;
self.toggle = function() {
self.open = !self.open;
};
$scope.$watch('isOpen', function(value, old) {
if (value === old) {
return;
}
self.open = value;
});
}
function link(scope, el, attrs, ctrl) {
var container = null;
var dest = null;
var tag = el.prop('tagName');
if (tag === 'TR' || tag === 'TH') {
// assumes table -> t(head|body) -> tr -> t(d|h)
var table = el.parent().parent();
var cols = maxColspan(table);
container = angular.element('<tr>');
dest = angular.element('<td>');
dest.attr('colspan', cols);
container.append(dest);
} else {
dest = angular.element('<div>');
container = dest;
}
container.addClass('nest');
if (scope.nest) {
// do some dirty hacks to make sure we pass the correct scope
//
// since we're appending as a sibling inside an ng-repeat, the effective
// scope when the template renders will be wrong even though we compiled
// and linked the element with our current scope
// as a workaround, we'll make our scope available to the template as
// 'scope' by literally passing '$parent' to the directive
var include = angular.element('<nest-transclude ' +
'template-url="' + scope.nest + '" ' +
'scope="$parent"></nest-transclude>');
$compile(include)(scope);
dest.append(include);
}
scope.$watch(function() { return ctrl.open; }, function(val, old) {
if (val) {
el.after(container);
} else {
container.remove();
}
});
}
return {
restrict: 'A',
scope: {
'isOpen': '=',
'nest': '@'
},
require: 'nest',
controller: controller,
controllerAs: 'nest',
link: link
};
}
directivesModule.directive('nest', nest);
function nestToggle() {
function link(scope, el, attrs, nestController) {
el.on('click', function() {
nestController.toggle();
scope.$apply();
});
}
return {
restrict: 'A',
require: '^^nest',
scope: true,
link: link
};
}
directivesModule.directive('nestToggle', nestToggle);
function nestIndicator() {
function link(scope, el, attrs, nestController) {
var fa = angular.element('<i></i>');
fa.addClass('fa fa-fw');
el.append(fa);
function update() {
if (fa.hasClass('fa-minus-square-o')) {
fa.removeClass('fa-minus-square-o');
}
if (fa.hasClass('fa-plus-square-o')) {
fa.removeClass('fa-plus-square-o');
}
if (nestController.open) {
fa.addClass('fa-minus-square-o');
} else {
fa.addClass('fa-plus-square-o');
}
}
scope.$watch(function() {
return nestController.open;
}, function(val, old) {
if (val === old) {
return;
}
update();
});
update();
}
return {
restrict: 'A',
require: '^^nest',
scope: true,
link: link
};
}
directivesModule.directive('nestIndicator', nestIndicator);
function nestTransclude() {
return {
restrict: 'EA',
transclude: true,
scope: {
'scope': '='
},
templateUrl: function(element, attrs) {
return attrs.templateUrl;
}
};
}
directivesModule.directive('nestTransclude', nestTransclude);

View File

@ -32,7 +32,11 @@ table.status-table {
table.default-cols {
thead {
th {
.tiny {
width: 1px;
}
th:not(.tiny) {
white-space: nowrap;
min-width: 75px;
}
@ -122,6 +126,10 @@ h1.page-header {
word-wrap: break-word;
}
h1.page-header {
word-wrap: break-word;
}
.feedback {
border: 1px solid #ccc;
background-color: #f5f5f5;
@ -136,3 +144,18 @@ h1.page-header {
padding: 1em;
z-index: 5;
}
[nest-toggle] {
cursor: pointer;
}
tr.nest {
nest-transclude table {
background-color: transparent !important;
margin-bottom: 0 !important;
}
> td {
padding: 0 !important;
}
}

View File

@ -61,40 +61,42 @@
</div>
<div class="row">
<div class="col-lg-12">
<div class="panel panel-default">
<div class="panel panel-default table-responsive">
<div class="panel-heading">
<h3 class="panel-title">Failed Tests in Last 10 Failed Runs</h3>
</div>
<div class="table-responsive">
<uib-accordion close-others="false">
<div ng-repeat="(key, value) in home.recentRuns">
<uib-accordion-group
template-url="templates/accordion-group-run.html"
heading="{{ key }};{{ value.bugs }}"
is-open="false">
<table table-sort data="value.fails"
class="table table-hover default-cols">
<thead>
<tr>
<th sort-field="test_id">Test ID</th>
<th sort-field="start_time">Start Time</th>
<th sort-default sort-field="stop_time">Stop Time</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="(k, v) in value.fails">
<td><a ui-sref="test({ testId: v.test_id })"}>
{{ v.test_id }}</a>
</td>
<td>{{ v.start_time | date:'M/d/yyyy HH:mm' }}</td>
<td>{{ v.stop_time | date:'M/d/yyyy HH:mm' }}</td>
</tr>
</tbody>
</table>
</uib-accordion-group>
</div>
</uib-accordion>
</div>
<table class="table table-hover default-cols">
<thead>
<tr>
<th class="tiny"></th>
<th>Job</th>
<th>Artifacts Link</th>
<th># Failed</th>
<th>Likely Bugs</th>
</tr>
</thead>
<tbody>
<tr nest="templates/run-details.html"
ng-repeat="(key, value) in home.recentRuns">
<td><span nest-toggle nest-indicator></span></td>
<td><a href nest-toggle>{{key | split:'/' | last:2 | pick:0}}</a></td>
<td>
<a target="_blank" href="{{ key }}">
{{key | split:'/' | last:2 | pick:1}}
<fa name="external-link"></fa>
</a>
</td>
<td>{{ value.fails.length }}</td>
<td>
<span ng-if="!!value.bugs">
{{ scope.value.bugs}}
</span>
<span ng-if="!value.bugs">-</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>

View File

@ -1,19 +0,0 @@
<div class="panel" ng-class="panelClass || 'panel-default'">
<div role="tab" id="{{::headingId}}" aria-selected="{{isOpen}}" class="panel-heading" ng-keypress="toggleOpen($event)">
<h4 class="panel-title">
<a role="button" data-toggle="collapse" href aria-expanded="{{isOpen}}" aria-controls="{{::panelId}}" tabindex="0" class="accordion-toggle" ng-click="toggleOpen()" uib-accordion-transclude="heading">
<span uib-accordion-header ng-class="{'text-muted': isDisabled}">
<i ng-class="{'fa fa-minus-square-o': isOpen, 'fa fa-plus-square-o': !isOpen}"></i>
{{heading | split:';' | first | join:'' | split:'/' | last:2 | join:'/'}}
</span>
</a>
<span class="text-info"><a target="_blank" href="{{heading | split:';' | first | join:''}}"><fa name="external-link"></fa></a></span>
<span class="pull-right">
{{heading | split:';' | last | join:''}}
</span>
</h4>
</div>
<div id="{{::panelId}}" aria-labelledby="{{::headingId}}" aria-hidden="{{!isOpen}}" role="tabpanel" class="panel-collapse collapse" uib-collapse="!isOpen">
<div class="panel-body" ng-transclude></div>
</div>
</div>

View File

@ -0,0 +1,20 @@
<table class="table">
<thead>
<tr>
<th sort-field="test_id">Test ID</th>
<th sort-field="start_time">Start Time</th>
<th sort-default sort-field="stop_time">Stop Time</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="(k, v) in scope.value.fails">
<td>
<a ui-sref="test({ testId: v.test_id })"}>
{{ v.test_id }}
</a>
</td>
<td>{{ v.start_time | date:'M/d/yyyy HH:mm' }}</td>
<td>{{ v.stop_time | date:'M/d/yyyy HH:mm' }}</td>
</tr>
</tbody>
</table>