Merge "Add search and filtering support to the timeline."
This commit is contained in:
@@ -9,6 +9,7 @@ function timelineOverview() {
|
|||||||
var margin = timelineController.margin;
|
var margin = timelineController.margin;
|
||||||
var height = 80;
|
var height = 80;
|
||||||
var laneHeight = 10;
|
var laneHeight = 10;
|
||||||
|
var loaded = false;
|
||||||
|
|
||||||
var x = timelineController.axes.x;
|
var x = timelineController.axes.x;
|
||||||
var y = d3.scale.linear();
|
var y = d3.scale.linear();
|
||||||
@@ -50,6 +51,17 @@ function timelineOverview() {
|
|||||||
.attr('stroke', 'rgba(100, 100, 100, 0.25)')
|
.attr('stroke', 'rgba(100, 100, 100, 0.25)')
|
||||||
.attr('fill', function(d) {
|
.attr('fill', function(d) {
|
||||||
return timelineController.statusColorMap[d.status];
|
return timelineController.statusColorMap[d.status];
|
||||||
|
})
|
||||||
|
.attr('class', function(d) {
|
||||||
|
if (timelineController.filterFunction) {
|
||||||
|
if (timelineController.filterFunction(d)) {
|
||||||
|
return 'filter-hit';
|
||||||
|
} else {
|
||||||
|
return 'filter-miss';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
rects.exit().remove();
|
rects.exit().remove();
|
||||||
@@ -145,6 +157,8 @@ function timelineOverview() {
|
|||||||
.attr('height', height - 1);
|
.attr('height', height - 1);
|
||||||
|
|
||||||
timelineController.setViewExtents(brush.extent());
|
timelineController.setViewExtents(brush.extent());
|
||||||
|
|
||||||
|
loaded = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
scope.$on('update', function() {
|
scope.$on('update', function() {
|
||||||
@@ -161,6 +175,13 @@ function timelineOverview() {
|
|||||||
shiftViewport(selection.item);
|
shiftViewport(selection.item);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
scope.$on('filter', function() {
|
||||||
|
if (loaded) {
|
||||||
|
console.log('filtering');
|
||||||
|
updateItems(timelineController.data);
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
98
app/js/directives/timeline-search.js
Normal file
98
app/js/directives/timeline-search.js
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var directivesModule = require('./_index.js');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ngInject
|
||||||
|
*/
|
||||||
|
function timelineSearch() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ngInject
|
||||||
|
*/
|
||||||
|
var controller = function($scope, $element) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
this.open = false;
|
||||||
|
this.query = '';
|
||||||
|
this.showSuccess = true;
|
||||||
|
this.showSkip = true;
|
||||||
|
this.showFail = true;
|
||||||
|
|
||||||
|
this.results = [];
|
||||||
|
|
||||||
|
var doFilter = function(item) {
|
||||||
|
if ((item.status === 'success' && !self.showSuccess) ||
|
||||||
|
(item.status === 'skip' && !self.showSkip) ||
|
||||||
|
(item.status === 'fail' && !self.showFail)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.name.toLowerCase().indexOf(self.query.toLowerCase()) < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.updateResults = function() {
|
||||||
|
var timeline = $element.controller('timeline');
|
||||||
|
timeline.setFilterFunction(function(item) {
|
||||||
|
return doFilter(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
var ret = [];
|
||||||
|
for (var i = 0; i < timeline.dataRaw.length; i++) {
|
||||||
|
var item = timeline.dataRaw[i];
|
||||||
|
|
||||||
|
if (!doFilter(item)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.push(timeline.dataRaw[i]);
|
||||||
|
if (ret.length > 25) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.results = ret;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.select = function(item) {
|
||||||
|
var timeline = $element.controller('timeline');
|
||||||
|
timeline.selectItem(item);
|
||||||
|
timeline.setFilterFunction(null);
|
||||||
|
|
||||||
|
self.query = '';
|
||||||
|
self.open = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
var update = function(a, b) {
|
||||||
|
if (a === b) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.updateResults();
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.$watch(function() { return self.query; }, update);
|
||||||
|
$scope.$watch(function() { return self.showSuccess; }, update);
|
||||||
|
$scope.$watch(function() { return self.showSkip; }, update);
|
||||||
|
$scope.$watch(function() { return self.showFail; }, update);
|
||||||
|
|
||||||
|
$scope.$on('dataLoaded', function() {
|
||||||
|
self.updateResults();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
restrict: 'EA',
|
||||||
|
require: ['^timelineSearch', '^timeline'],
|
||||||
|
scope: true,
|
||||||
|
controller: controller,
|
||||||
|
controllerAs: 'search',
|
||||||
|
templateUrl: 'directives/timeline-search.html'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
directivesModule.directive('timelineSearch', timelineSearch);
|
@@ -201,6 +201,17 @@ function timelineViewport($document) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.attr('class', function(d) {
|
||||||
|
if (timelineController.filterFunction) {
|
||||||
|
if (timelineController.filterFunction(d)) {
|
||||||
|
return 'filter-hit';
|
||||||
|
} else {
|
||||||
|
return 'filter-miss';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
.on("mouseover", rectMouseOver)
|
.on("mouseover", rectMouseOver)
|
||||||
.on('mouseout', rectMouseOut)
|
.on('mouseout', rectMouseOut)
|
||||||
.on('click', rectClick);
|
.on('click', rectClick);
|
||||||
@@ -308,6 +319,12 @@ function timelineViewport($document) {
|
|||||||
select(null);
|
select(null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
scope.$on('filter', function() {
|
||||||
|
if (loaded) {
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@@ -56,6 +56,7 @@ function timeline($log, datasetService) {
|
|||||||
self.selectionName = null;
|
self.selectionName = null;
|
||||||
self.selection = null;
|
self.selection = null;
|
||||||
self.hover = null;
|
self.hover = null;
|
||||||
|
self.filterFunction = null;
|
||||||
|
|
||||||
self.setViewExtents = function(extents) {
|
self.setViewExtents = function(extents) {
|
||||||
if (angular.isNumber(extents[0])) {
|
if (angular.isNumber(extents[0])) {
|
||||||
@@ -104,6 +105,12 @@ function timeline($log, datasetService) {
|
|||||||
$scope.$broadcast('postSelect', self.selection);
|
$scope.$broadcast('postSelect', self.selection);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
self.setFilterFunction = function(fn) {
|
||||||
|
self.filterFunction = fn;
|
||||||
|
|
||||||
|
$scope.$broadcast('filter', fn);
|
||||||
|
};
|
||||||
|
|
||||||
self.selectItem = function(item) {
|
self.selectItem = function(item) {
|
||||||
var workerItems = self.data[item.worker].values;
|
var workerItems = self.data[item.worker].values;
|
||||||
var index = -1;
|
var index = -1;
|
||||||
@@ -249,9 +256,14 @@ function timeline($log, datasetService) {
|
|||||||
|
|
||||||
var link = function(scope, el, attrs, ctrl) {
|
var link = function(scope, el, attrs, ctrl) {
|
||||||
var updateWidth = function() {
|
var updateWidth = function() {
|
||||||
ctrl.width = el.parent()[0].clientWidth -
|
var body = el[0].querySelector('div.panel div.panel-body');
|
||||||
|
var style = getComputedStyle(body);
|
||||||
|
|
||||||
|
ctrl.width = body.clientWidth -
|
||||||
ctrl.margin.left -
|
ctrl.margin.left -
|
||||||
ctrl.margin.right;
|
ctrl.margin.right -
|
||||||
|
parseFloat(style.paddingLeft) -
|
||||||
|
parseFloat(style.paddingRight);
|
||||||
};
|
};
|
||||||
|
|
||||||
scope.$on('windowResize', updateWidth);
|
scope.$on('windowResize', updateWidth);
|
||||||
@@ -276,7 +288,7 @@ function timeline($log, datasetService) {
|
|||||||
controllerAs: 'timeline',
|
controllerAs: 'timeline',
|
||||||
restrict: 'EA',
|
restrict: 'EA',
|
||||||
transclude: true,
|
transclude: true,
|
||||||
template: '<ng-transclude></ng-transclude>',
|
templateUrl: 'directives/timeline.html',
|
||||||
scope: {
|
scope: {
|
||||||
'dataset': '=',
|
'dataset': '=',
|
||||||
'hoveredItem': '=',
|
'hoveredItem': '=',
|
||||||
|
24
app/js/filters/bootstrap-filters.js
vendored
Normal file
24
app/js/filters/bootstrap-filters.js
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var filtersModule = require('./_index.js');
|
||||||
|
|
||||||
|
var contextClass = function(test, type) {
|
||||||
|
var clazz;
|
||||||
|
if (test.status === 'success') {
|
||||||
|
clazz = 'success';
|
||||||
|
} else if (test.status === 'skip') {
|
||||||
|
clazz = 'info';
|
||||||
|
} else if (test.status === 'fail') {
|
||||||
|
clazz = 'danger';
|
||||||
|
} else {
|
||||||
|
clazz = 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type) {
|
||||||
|
return type + '-' + clazz;
|
||||||
|
} else {
|
||||||
|
return clazz;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
filtersModule.filter('contextClass', function() { return contextClass; });
|
9
app/styles/directives/_timeline-overview.scss
Normal file
9
app/styles/directives/_timeline-overview.scss
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
timeline-overview svg {
|
||||||
|
.filter-hit {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-miss {
|
||||||
|
opacity: 0.15;
|
||||||
|
}
|
||||||
|
}
|
28
app/styles/directives/_timeline-search.scss
Normal file
28
app/styles/directives/_timeline-search.scss
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
timeline-search {
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
.popover {
|
||||||
|
max-width: 500px;
|
||||||
|
|
||||||
|
.timeline-search-popover {
|
||||||
|
width: 300px;
|
||||||
|
.input-group {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-group label {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jump-group li {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jump-group ul {
|
||||||
|
max-height: 20em;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
app/styles/directives/_timeline-viewport.scss
Normal file
9
app/styles/directives/_timeline-viewport.scss
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
timeline-viewport svg {
|
||||||
|
.filter-hit {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-miss {
|
||||||
|
opacity: 0.15;
|
||||||
|
}
|
||||||
|
}
|
@@ -5,3 +5,6 @@
|
|||||||
@import 'sb-admin-2';
|
@import 'sb-admin-2';
|
||||||
|
|
||||||
@import 'directives/_timeline-details.scss';
|
@import 'directives/_timeline-details.scss';
|
||||||
|
@import 'directives/_timeline-search.scss';
|
||||||
|
@import 'directives/_timeline-viewport.scss';
|
||||||
|
@import 'directives/_timeline-overview.scss';
|
||||||
|
@@ -9,16 +9,13 @@
|
|||||||
|
|
||||||
<div ng-if="!!item"
|
<div ng-if="!!item"
|
||||||
class="panel"
|
class="panel"
|
||||||
ng-class="{'panel-success': item.status == 'success', 'panel-info': item.status == 'skip', 'panel-danger': item.status == 'fail'}">
|
ng-class="item | contextClass:'panel'">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h3 class="panel-title">
|
<h3 class="panel-title">
|
||||||
Details: {{item.name | split:'.' | pickRight:1}}
|
Details: {{item.name | split:'.' | pickRight:1}}
|
||||||
<span class="label label-success"
|
<span class="label label-{{item | contextClass}}">
|
||||||
ng-if="item.status == 'success'">success</span>
|
{{item.status}}
|
||||||
<span class="label label-info"
|
</span>
|
||||||
ng-if="item.status == 'skip'">skip</span>
|
|
||||||
<span class="label label-danger"
|
|
||||||
ng-if="item.status == 'fail'">fail</span>
|
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-bordered table-hover table-striped">
|
<table class="table table-bordered table-hover table-striped">
|
||||||
|
38
app/views/directives/timeline-search-popover.html
Normal file
38
app/views/directives/timeline-search-popover.html
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<div class="timeline-search-popover">
|
||||||
|
<label>Filter by name:</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-addon"><fa name="search"></fa></span>
|
||||||
|
<input type="text"
|
||||||
|
placeholder="query..."
|
||||||
|
class="form-control"
|
||||||
|
ng-model="search.query"
|
||||||
|
ng-model-options="{debounce: 250}">
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<label>Filter by status:</label>
|
||||||
|
<div class="status-group btn-group btn-group-justified">
|
||||||
|
<label uib-btn-checkbox
|
||||||
|
ng-model="search.showSuccess"
|
||||||
|
class="btn btn-default">success</label>
|
||||||
|
<label uib-btn-checkbox
|
||||||
|
ng-model="search.showSkip"
|
||||||
|
class="btn btn-default">skip</label>
|
||||||
|
<label uib-btn-checkbox
|
||||||
|
ng-model="search.showFail"
|
||||||
|
class="btn btn-default">fail</label>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<div class="jump-group">
|
||||||
|
<label>Jump to:</label>
|
||||||
|
<ul class="list-group">
|
||||||
|
<li ng-repeat="item in search.results"
|
||||||
|
class="list-group-item"
|
||||||
|
ng-class="item | contextClass:'list-group-item'"
|
||||||
|
ng-click="search.select(item)">
|
||||||
|
{{item.name | split:'.' | pickRight:1}}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
7
app/views/directives/timeline-search.html
Normal file
7
app/views/directives/timeline-search.html
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<a uib-popover-template="'directives/timeline-search-popover.html'"
|
||||||
|
popover-placement="bottom-right"
|
||||||
|
popover-title="Filter Options"
|
||||||
|
popover-is-open="search.open">
|
||||||
|
<fa name="search"></fa>
|
||||||
|
<fa name="caret-down"></fa>
|
||||||
|
</a>
|
13
app/views/directives/timeline.html
Normal file
13
app/views/directives/timeline.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">
|
||||||
|
Timeline
|
||||||
|
<timeline-search class="pull-right"></timeline-search>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<timeline-viewport></timeline-viewport>
|
||||||
|
<timeline-dstat></timeline-dstat>
|
||||||
|
<timeline-overview></timeline-overview>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -17,20 +17,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row" ng-if="!timeline.error">
|
<div class="row" ng-if="!timeline.error">
|
||||||
<div class="col-lg-12">
|
<div class="col-lg-12">
|
||||||
<div class="panel panel-default">
|
<timeline dataset="timeline.dataset"
|
||||||
<div class="panel-heading">
|
hovered-item="timeline.hoveredItem"
|
||||||
<h3 class="panel-title">Timeline</h3>
|
selected-item="timeline.selectedItem"
|
||||||
</div>
|
preselect="timeline.preselect"></timeline>
|
||||||
<timeline class="panel-body"
|
|
||||||
dataset="timeline.dataset"
|
|
||||||
hovered-item="timeline.hoveredItem"
|
|
||||||
selected-item="timeline.selectedItem"
|
|
||||||
preselect="timeline.preselect">
|
|
||||||
<timeline-viewport></timeline-viewport>
|
|
||||||
<timeline-dstat></timeline-dstat>
|
|
||||||
<timeline-overview></timeline-overview>
|
|
||||||
</timeline>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
Reference in New Issue
Block a user