
This adds a new UI for searching and filtering through tests in a timeline. A new dropdown for filter options is added to the timeline panel header, where users can query and select tests based on name and metadata (pass/fail/skip). A list of results is displayed which can be selected from directly, but results are also highlighted on the timeline directly. Some rearchitecting of the HTML layout for the timeline directive was needed to allow part of the timeline to be inside a panel header, so the entire panel layout was moved inside the timeline directive. A new `filterFunction` field was added to the main timeline controller to support communicating the filtering parameters to other components of the timeline. Additionally, a new `contextClass` filter was added to avoid excessive code duplication for highlighting element color based on test status - existing uses were replaced with this. Change-Id: I5f35091ab2b605e0821125e79de47c4c6067f644
303 lines
7.3 KiB
JavaScript
303 lines
7.3 KiB
JavaScript
'use strict';
|
|
|
|
var directivesModule = require('./_index.js');
|
|
|
|
var arrayUtil = require('../util/array-util');
|
|
var parseDstat = require('../util/dstat-parse');
|
|
var d3 = require('d3');
|
|
|
|
var statusColorMap = {
|
|
'success': 'LightGreen',
|
|
'fail': 'Crimson',
|
|
'skip': 'DodgerBlue',
|
|
'selected': 'GoldenRod',
|
|
'hover': 'DarkTurquoise'
|
|
};
|
|
|
|
var parseWorker = function(tags) {
|
|
for (var i = 0; i < tags.length; i++) {
|
|
if (!tags[i].startsWith('worker')) {
|
|
continue;
|
|
}
|
|
|
|
return parseInt(tags[i].split('-')[1]);
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* @ngInject
|
|
*/
|
|
function timeline($log, datasetService) {
|
|
|
|
/**
|
|
* @ngInject
|
|
*/
|
|
var controller = function($scope) {
|
|
var self = this;
|
|
self.statusColorMap = statusColorMap;
|
|
|
|
self.data = [];
|
|
self.dataRaw = [];
|
|
self.dstat = [];
|
|
|
|
self.margin = { top: 40, right: 10, bottom: 10, left: 80 };
|
|
self.width = 0;
|
|
self.height = 550 - this.margin.top - this.margin.bottom;
|
|
|
|
self.timeExtents = [0, 0];
|
|
self.viewExtents = [0, 0];
|
|
self.axes = {
|
|
x: d3.time.scale(),
|
|
selection: d3.scale.linear()
|
|
};
|
|
|
|
self.selectionName = null;
|
|
self.selection = null;
|
|
self.hover = null;
|
|
self.filterFunction = null;
|
|
|
|
self.setViewExtents = function(extents) {
|
|
if (angular.isNumber(extents[0])) {
|
|
extents[0] = new Date(extents[0]);
|
|
}
|
|
|
|
if (angular.isNumber(extents[1])) {
|
|
extents[1] = new Date(extents[1]);
|
|
}
|
|
|
|
self.viewExtents = extents;
|
|
self.axes.selection.domain(extents);
|
|
|
|
$scope.$broadcast('updateView');
|
|
};
|
|
|
|
self.setHover = function(item) {
|
|
self.hover = item;
|
|
$scope.hoveredItem = item;
|
|
};
|
|
|
|
self.clearHover = function() {
|
|
self.hover = null;
|
|
$scope.hoveredItem = null;
|
|
};
|
|
|
|
self.setSelection = function(index, item) {
|
|
if (self.selection && self.selection.item.name === item.name) {
|
|
self.selectionName = null;
|
|
self.selection = null;
|
|
$scope.selectedItem = null;
|
|
} else {
|
|
self.selectionName = item.name;
|
|
self.selection = {
|
|
item: item,
|
|
index: index
|
|
};
|
|
$scope.selectedItem = item;
|
|
}
|
|
|
|
// selection in the viewport depends on the overview setting the view
|
|
// extents & makings sure there is a visible rect to select
|
|
// the postSelect event makes sure that this is handled in the correct
|
|
// sequence
|
|
$scope.$broadcast('select', self.selection);
|
|
$scope.$broadcast('postSelect', self.selection);
|
|
};
|
|
|
|
self.setFilterFunction = function(fn) {
|
|
self.filterFunction = fn;
|
|
|
|
$scope.$broadcast('filter', fn);
|
|
};
|
|
|
|
self.selectItem = function(item) {
|
|
var workerItems = self.data[item.worker].values;
|
|
var index = -1;
|
|
|
|
workerItems.forEach(function(d, i) {
|
|
if (d.name === item.name) {
|
|
index = i;
|
|
}
|
|
});
|
|
|
|
if (index === -1) {
|
|
return false;
|
|
}
|
|
|
|
self.setSelection(index, item);
|
|
return true;
|
|
};
|
|
|
|
self.selectIndex = function(worker, index) {
|
|
var item = self.data[worker].values[index];
|
|
|
|
self.setSelection(index, item);
|
|
return true;
|
|
};
|
|
|
|
self.clearSelection = function() {
|
|
self.selection = null;
|
|
$scope.$broadcast('select', null);
|
|
};
|
|
|
|
self.selectNextItem = function() {
|
|
if (self.selection) {
|
|
var worker = self.selection.item.worker;
|
|
if (self.selection.index < self.data[worker].values.length - 1) {
|
|
self.selectIndex(worker, (self.selection.index) + 1);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
self.selectPreviousItem = function() {
|
|
if (self.selection) {
|
|
var worker = self.selection.item.worker;
|
|
if (self.selection.index > 0) {
|
|
self.selectIndex(worker, (self.selection.index) - 1);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
var initData = function(raw) {
|
|
self.dataRaw = raw;
|
|
|
|
var minStart = null;
|
|
var maxEnd = null;
|
|
var preselect = null;
|
|
|
|
// parse date strings and determine extents
|
|
raw.forEach(function(d) {
|
|
d.worker = parseWorker(d.tags);
|
|
|
|
d.startDate = new Date(d.timestamps[0]);
|
|
if (minStart === null || d.startDate < minStart) {
|
|
minStart = d.startDate;
|
|
}
|
|
|
|
d.endDate = new Date(d.timestamps[1]);
|
|
if (maxEnd === null || d.endDate > maxEnd) {
|
|
maxEnd = d.endDate;
|
|
}
|
|
|
|
if ($scope.preselect && d.name === $scope.preselect) {
|
|
preselect = d;
|
|
}
|
|
});
|
|
|
|
self.timeExtents = [ minStart, maxEnd ];
|
|
|
|
self.data = d3.nest()
|
|
.key(function(d) { return d.worker; })
|
|
.sortKeys(d3.ascending)
|
|
.entries(raw.filter(function(d) { return d.duration > 0; }));
|
|
|
|
self.axes.x.domain(self.timeExtents);
|
|
|
|
$scope.$broadcast('dataLoaded', self.data);
|
|
|
|
if (preselect) {
|
|
self.selectItem(preselect);
|
|
}
|
|
};
|
|
|
|
var initDstat = function(raw) {
|
|
var min = self.timeExtents[0];
|
|
var max = self.timeExtents[1];
|
|
|
|
var accessor = function(d) { return d.system_time; };
|
|
var minIndex = arrayUtil.binaryMinIndex(min, raw.entries, accessor);
|
|
var maxIndex = arrayUtil.binaryMaxIndex(max, raw.entries, accessor);
|
|
|
|
self.dstat = {
|
|
entries: raw.entries.slice(minIndex, maxIndex),
|
|
minimums: raw.minimums,
|
|
maximums: raw.maximums
|
|
};
|
|
|
|
$scope.$broadcast('dstatLoaded', self.dstat);
|
|
};
|
|
|
|
$scope.$watch('dataset', function(dataset) {
|
|
if (!dataset) {
|
|
return;
|
|
}
|
|
|
|
// load dataset details (raw log entries and dstat) sequentially
|
|
// we need to determine the initial date from the subunit data to parse
|
|
// dstat
|
|
datasetService.raw(dataset).then(function(response) {
|
|
initData(response.data);
|
|
|
|
return datasetService.dstat(dataset);
|
|
}).then(function(response) {
|
|
var firstDate = new Date(self.dataRaw[0].timestamps[0]);
|
|
|
|
var raw = parseDstat(response.data, firstDate.getYear());
|
|
initDstat(raw);
|
|
|
|
$scope.$broadcast('update');
|
|
}).catch(function(ex) {
|
|
$log.error(ex);
|
|
});
|
|
});
|
|
|
|
$scope.$watch(function() { return self.width; }, function(width) {
|
|
self.axes.x.range([0, width]);
|
|
self.axes.selection.range([0, width]);
|
|
|
|
$scope.$broadcast('update');
|
|
});
|
|
};
|
|
|
|
var link = function(scope, el, attrs, ctrl) {
|
|
var updateWidth = function() {
|
|
var body = el[0].querySelector('div.panel div.panel-body');
|
|
var style = getComputedStyle(body);
|
|
|
|
ctrl.width = body.clientWidth -
|
|
ctrl.margin.left -
|
|
ctrl.margin.right -
|
|
parseFloat(style.paddingLeft) -
|
|
parseFloat(style.paddingRight);
|
|
};
|
|
|
|
scope.$on('windowResize', updateWidth);
|
|
updateWidth();
|
|
|
|
d3.select(window)
|
|
.on("keydown", function() {
|
|
var code = d3.event.keyCode;
|
|
if (code == 37) {
|
|
ctrl.selectPreviousItem();
|
|
}
|
|
else if (code == 39) {
|
|
ctrl.selectNextItem();
|
|
}
|
|
scope.$apply();
|
|
});
|
|
|
|
};
|
|
|
|
return {
|
|
controller: controller,
|
|
controllerAs: 'timeline',
|
|
restrict: 'EA',
|
|
transclude: true,
|
|
templateUrl: 'directives/timeline.html',
|
|
scope: {
|
|
'dataset': '=',
|
|
'hoveredItem': '=',
|
|
'selectedItem': '=',
|
|
'preselect': '='
|
|
},
|
|
link: link
|
|
};
|
|
}
|
|
|
|
directivesModule.directive('timeline', timeline);
|