
Previously, tests hidden due to their small (< 2px) size would be silently hidden from view, and navigating to a hidden test (via direct link, search, or keyboard) would move the timeline to the correct position, but would show only empty space, even though the details panel would be updated. This adds a label and a warning message to the details panel to inform users that the selected test is too small to show in the view. Additionally, when the selection is changed to a hidden item, any previously-selected rect will now be properly restored to its original color. Change-Id: I3f6ee9313db64472ce8f6ce7d929c4d464f12ab5
317 lines
7.7 KiB
JavaScript
317 lines
7.7 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, progressService) {
|
|
|
|
/**
|
|
* @ngInject
|
|
*/
|
|
var controller = function($scope) {
|
|
var self = this;
|
|
self.statusColorMap = statusColorMap;
|
|
|
|
self.data = [];
|
|
self.dataRaw = [];
|
|
self.dstat = [];
|
|
|
|
self.margin = { top: 40, right: 10, bottom: 20, 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;
|
|
};
|
|
|
|
self.hidden = function(item) {
|
|
var width = self.axes.selection(item.endDate) -
|
|
self.axes.selection(item.startDate);
|
|
var hidden = width < 2;
|
|
item.hidden = hidden;
|
|
|
|
return hidden;
|
|
};
|
|
|
|
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;
|
|
}
|
|
|
|
progressService.start({ parent: 'timeline .panel-body' });
|
|
|
|
// 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) {
|
|
progressService.set(0.33);
|
|
initData(response.data);
|
|
|
|
return datasetService.dstat(dataset);
|
|
}).then(function(response) {
|
|
progressService.set(0.66);
|
|
var firstDate = new Date(self.dataRaw[0].timestamps[0]);
|
|
|
|
var raw = parseDstat(response.data, firstDate.getYear());
|
|
initDstat(raw);
|
|
|
|
$scope.$broadcast('update');
|
|
progressService.done();
|
|
}).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);
|