blob: 03c887b9310f2729805a6432fc6036e09e5e08be [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* EventsView displays a filtered list of all events sharing a source, and
* a details pane for the selected sources.
*
* +----------------------++----------------+
* | filter box || |
* +----------------------+| |
* | || |
* | || |
* | || |
* | || |
* | source list || details |
* | || view |
* | || |
* | || |
* | || |
* | || |
* | || |
* | || |
* +----------------------++----------------+
*/
var EventsView = (function() {
'use strict';
// How soon after updating the filter list the counter should be updated.
var REPAINT_FILTER_COUNTER_TIMEOUT_MS = 0;
// We inherit from View.
var superClass = View;
/*
* @constructor
*/
function EventsView() {
assertFirstConstructorCall(EventsView);
// Call superclass's constructor.
superClass.call(this);
// Initialize the sub-views.
var leftPane = new VerticalSplitView(new DivView(EventsView.TOPBAR_ID),
new DivView(EventsView.LIST_BOX_ID));
this.detailsView_ = new DetailsView(EventsView.DETAILS_LOG_BOX_ID);
this.splitterView_ = new ResizableVerticalSplitView(
leftPane, this.detailsView_, new DivView(EventsView.SIZER_ID));
SourceTracker.getInstance().addSourceEntryObserver(this);
this.tableBody_ = $(EventsView.TBODY_ID);
this.filterInput_ = $(EventsView.FILTER_INPUT_ID);
this.filterCount_ = $(EventsView.FILTER_COUNT_ID);
this.filterInput_.addEventListener('search',
this.onFilterTextChanged_.bind(this), true);
$(EventsView.SELECT_ALL_ID).addEventListener(
'click', this.selectAll_.bind(this), true);
$(EventsView.SORT_BY_ID_ID).addEventListener(
'click', this.sortById_.bind(this), true);
$(EventsView.SORT_BY_SOURCE_TYPE_ID).addEventListener(
'click', this.sortBySourceType_.bind(this), true);
$(EventsView.SORT_BY_DESCRIPTION_ID).addEventListener(
'click', this.sortByDescription_.bind(this), true);
new MouseOverHelp(EventsView.FILTER_HELP_ID,
EventsView.FILTER_HELP_HOVER_ID);
// Sets sort order and filter.
this.setFilter_('');
this.initializeSourceList_();
}
EventsView.TAB_ID = 'tab-handle-events';
EventsView.TAB_NAME = 'Events';
EventsView.TAB_HASH = '#events';
// IDs for special HTML elements in events_view.html
EventsView.TBODY_ID = 'events-view-source-list-tbody';
EventsView.FILTER_INPUT_ID = 'events-view-filter-input';
EventsView.FILTER_COUNT_ID = 'events-view-filter-count';
EventsView.FILTER_HELP_ID = 'events-view-filter-help';
EventsView.FILTER_HELP_HOVER_ID = 'events-view-filter-help-hover';
EventsView.SELECT_ALL_ID = 'events-view-select-all';
EventsView.SORT_BY_ID_ID = 'events-view-sort-by-id';
EventsView.SORT_BY_SOURCE_TYPE_ID = 'events-view-sort-by-source';
EventsView.SORT_BY_DESCRIPTION_ID = 'events-view-sort-by-description';
EventsView.DETAILS_LOG_BOX_ID = 'events-view-details-log-box';
EventsView.TOPBAR_ID = 'events-view-filter-box';
EventsView.LIST_BOX_ID = 'events-view-source-list';
EventsView.SIZER_ID = 'events-view-splitter-box';
cr.addSingletonGetter(EventsView);
EventsView.prototype = {
// Inherit the superclass's methods.
__proto__: superClass.prototype,
/**
* Initializes the list of source entries. If source entries are already,
* being displayed, removes them all in the process.
*/
initializeSourceList_: function() {
this.currentSelectedRows_ = [];
this.sourceIdToRowMap_ = {};
this.tableBody_.innerHTML = '';
this.numPrefilter_ = 0;
this.numPostfilter_ = 0;
this.invalidateFilterCounter_();
this.invalidateDetailsView_();
},
setGeometry: function(left, top, width, height) {
superClass.prototype.setGeometry.call(this, left, top, width, height);
this.splitterView_.setGeometry(left, top, width, height);
},
show: function(isVisible) {
superClass.prototype.show.call(this, isVisible);
this.splitterView_.show(isVisible);
},
getFilterText_: function() {
return this.filterInput_.value;
},
setFilterText_: function(filterText) {
this.filterInput_.value = filterText;
this.onFilterTextChanged_();
},
onFilterTextChanged_: function() {
this.setFilter_(this.getFilterText_());
},
/**
* Updates text in the details view when privacy stripping is toggled.
*/
onPrivacyStrippingChanged: function() {
this.invalidateDetailsView_();
},
/**
* Updates text in the details view when time display mode is toggled.
*/
onUseRelativeTimesChanged: function() {
this.invalidateDetailsView_();
},
comparisonFuncWithReversing_: function(a, b) {
var result = this.comparisonFunction_(a, b);
if (this.doSortBackwards_)
result *= -1;
return result;
},
sort_: function() {
var sourceEntries = [];
for (var id in this.sourceIdToRowMap_) {
sourceEntries.push(this.sourceIdToRowMap_[id].getSourceEntry());
}
sourceEntries.sort(this.comparisonFuncWithReversing_.bind(this));
// Reposition source rows from back to front.
for (var i = sourceEntries.length - 2; i >= 0; --i) {
var sourceRow = this.sourceIdToRowMap_[sourceEntries[i].getSourceId()];
var nextSourceId = sourceEntries[i + 1].getSourceId();
if (sourceRow.getNextNodeSourceId() != nextSourceId) {
var nextSourceRow = this.sourceIdToRowMap_[nextSourceId];
sourceRow.moveBefore(nextSourceRow);
}
}
},
setFilter_: function(filterText) {
var lastComparisonFunction = this.comparisonFunction_;
var lastDoSortBackwards = this.doSortBackwards_;
var filterParser = new SourceFilterParser(filterText);
this.currentFilter_ = filterParser.filter;
this.pickSortFunction_(filterParser.sort);
if (lastComparisonFunction != this.comparisonFunction_ ||
lastDoSortBackwards != this.doSortBackwards_) {
this.sort_();
}
// Iterate through all of the rows and see if they match the filter.
for (var id in this.sourceIdToRowMap_) {
var entry = this.sourceIdToRowMap_[id];
entry.setIsMatchedByFilter(this.currentFilter_(entry.getSourceEntry()));
}
},
/**
* Given a "sort" object with "method" and "backwards" keys, looks up and
* sets |comparisonFunction_| and |doSortBackwards_|. If the ID does not
* correspond to a sort function, defaults to sorting by ID.
*/
pickSortFunction_: function(sort) {
this.doSortBackwards_ = sort.backwards;
this.comparisonFunction_ = COMPARISON_FUNCTION_TABLE[sort.method];
if (!this.comparisonFunction_) {
this.doSortBackwards_ = false;
this.comparisonFunction_ = compareSourceId_;
}
},
/**
* Repositions |sourceRow|'s in the table using an insertion sort.
* Significantly faster than sorting the entire table again, when only
* one entry has changed.
*/
insertionSort_: function(sourceRow) {
// SourceRow that should be after |sourceRow|, if it needs
// to be moved earlier in the list.
var sourceRowAfter = sourceRow;
while (true) {
var prevSourceId = sourceRowAfter.getPreviousNodeSourceId();
if (prevSourceId == null)
break;
var prevSourceRow = this.sourceIdToRowMap_[prevSourceId];
if (this.comparisonFuncWithReversing_(
sourceRow.getSourceEntry(),
prevSourceRow.getSourceEntry()) >= 0) {
break;
}
sourceRowAfter = prevSourceRow;
}
if (sourceRowAfter != sourceRow) {
sourceRow.moveBefore(sourceRowAfter);
return;
}
var sourceRowBefore = sourceRow;
while (true) {
var nextSourceId = sourceRowBefore.getNextNodeSourceId();
if (nextSourceId == null)
break;
var nextSourceRow = this.sourceIdToRowMap_[nextSourceId];
if (this.comparisonFuncWithReversing_(
sourceRow.getSourceEntry(),
nextSourceRow.getSourceEntry()) <= 0) {
break;
}
sourceRowBefore = nextSourceRow;
}
if (sourceRowBefore != sourceRow)
sourceRow.moveAfter(sourceRowBefore);
},
/**
* Called whenever SourceEntries are updated with new log entries. Updates
* the corresponding table rows, sort order, and the details view as needed.
*/
onSourceEntriesUpdated: function(sourceEntries) {
var isUpdatedSourceSelected = false;
var numNewSourceEntries = 0;
for (var i = 0; i < sourceEntries.length; ++i) {
var sourceEntry = sourceEntries[i];
// Lookup the row.
var sourceRow = this.sourceIdToRowMap_[sourceEntry.getSourceId()];
if (!sourceRow) {
sourceRow = new SourceRow(this, sourceEntry);
this.sourceIdToRowMap_[sourceEntry.getSourceId()] = sourceRow;
++numNewSourceEntries;
} else {
sourceRow.onSourceUpdated();
}
if (sourceRow.isSelected())
isUpdatedSourceSelected = true;
// TODO(mmenke): Fix sorting when sorting by duration.
// Duration continuously increases for all entries that
// are still active. This can result in incorrect
// sorting, until sort_ is called.
this.insertionSort_(sourceRow);
}
if (isUpdatedSourceSelected)
this.invalidateDetailsView_();
if (numNewSourceEntries)
this.incrementPrefilterCount(numNewSourceEntries);
},
/**
* Returns the SourceRow with the specified ID, if there is one.
* Otherwise, returns undefined.
*/
getSourceRow: function(id) {
return this.sourceIdToRowMap_[id];
},
/**
* Called whenever all log events are deleted.
*/
onAllSourceEntriesDeleted: function() {
this.initializeSourceList_();
},
/**
* Called when either a log file is loaded, after clearing the old entries,
* but before getting any new ones.
*/
onLoadLogStart: function() {
// Needed to sort new sourceless entries correctly.
this.maxReceivedSourceId_ = 0;
},
onLoadLogFinish: function(data) {
return true;
},
incrementPrefilterCount: function(offset) {
this.numPrefilter_ += offset;
this.invalidateFilterCounter_();
},
incrementPostfilterCount: function(offset) {
this.numPostfilter_ += offset;
this.invalidateFilterCounter_();
},
onSelectionChanged: function() {
this.invalidateDetailsView_();
},
clearSelection: function() {
var prevSelection = this.currentSelectedRows_;
this.currentSelectedRows_ = [];
// Unselect everything that is currently selected.
for (var i = 0; i < prevSelection.length; ++i) {
prevSelection[i].setSelected(false);
}
this.onSelectionChanged();
},
selectAll_: function(event) {
for (var id in this.sourceIdToRowMap_) {
var sourceRow = this.sourceIdToRowMap_[id];
if (sourceRow.isMatchedByFilter()) {
sourceRow.setSelected(true);
}
}
event.preventDefault();
},
unselectAll_: function() {
var entries = this.currentSelectedRows_.slice(0);
for (var i = 0; i < entries.length; ++i) {
entries[i].setSelected(false);
}
},
/**
* If |params| includes a query, replaces the current filter and unselects.
* all items. If it includes a selection, tries to select the relevant
* item.
*/
setParameters: function(params) {
if (params.q) {
this.unselectAll_();
this.setFilterText_(params.q);
}
if (params.s) {
var sourceRow = this.sourceIdToRowMap_[params.s];
if (sourceRow) {
sourceRow.setSelected(true);
this.scrollToSourceId(params.s);
}
}
},
/**
* Scrolls to the source indicated by |sourceId|, if displayed.
*/
scrollToSourceId: function(sourceId) {
this.detailsView_.scrollToSourceId(sourceId);
},
/**
* If already using the specified sort method, flips direction. Otherwise,
* removes pre-existing sort parameter before adding the new one.
*/
toggleSortMethod_: function(sortMethod) {
// Get old filter text and remove old sort directives, if any.
var filterParser = new SourceFilterParser(this.getFilterText_());
var filterText = filterParser.filterTextWithoutSort;
filterText = 'sort:' + sortMethod + ' ' + filterText;
// If already using specified sortMethod, sort backwards.
if (!this.doSortBackwards_ &&
COMPARISON_FUNCTION_TABLE[sortMethod] == this.comparisonFunction_) {
filterText = '-' + filterText;
}
this.setFilterText_(filterText.trim());
},
sortById_: function(event) {
this.toggleSortMethod_('id');
},
sortBySourceType_: function(event) {
this.toggleSortMethod_('source');
},
sortByDescription_: function(event) {
this.toggleSortMethod_('desc');
},
/**
* Modifies the map of selected rows to include/exclude the one with
* |sourceId|, if present. Does not modify checkboxes or the LogView.
* Should only be called by a SourceRow in response to its selection
* state changing.
*/
modifySelectionArray: function(sourceId, addToSelection) {
var sourceRow = this.sourceIdToRowMap_[sourceId];
if (!sourceRow)
return;
// Find the index for |sourceEntry| in the current selection list.
var index = -1;
for (var i = 0; i < this.currentSelectedRows_.length; ++i) {
if (this.currentSelectedRows_[i] == sourceRow) {
index = i;
break;
}
}
if (index != -1 && !addToSelection) {
// Remove from the selection.
this.currentSelectedRows_.splice(index, 1);
}
if (index == -1 && addToSelection) {
this.currentSelectedRows_.push(sourceRow);
}
},
getSelectedSourceEntries_: function() {
var sourceEntries = [];
for (var i = 0; i < this.currentSelectedRows_.length; ++i) {
sourceEntries.push(this.currentSelectedRows_[i].getSourceEntry());
}
return sourceEntries;
},
invalidateDetailsView_: function() {
this.detailsView_.setData(this.getSelectedSourceEntries_());
},
invalidateFilterCounter_: function() {
if (!this.outstandingRepaintFilterCounter_) {
this.outstandingRepaintFilterCounter_ = true;
window.setTimeout(this.repaintFilterCounter_.bind(this),
REPAINT_FILTER_COUNTER_TIMEOUT_MS);
}
},
repaintFilterCounter_: function() {
this.outstandingRepaintFilterCounter_ = false;
this.filterCount_.innerHTML = '';
addTextNode(this.filterCount_,
this.numPostfilter_ + ' of ' + this.numPrefilter_);
}
}; // end of prototype.
// ------------------------------------------------------------------------
// Helper code for comparisons
// ------------------------------------------------------------------------
var COMPARISON_FUNCTION_TABLE = {
// sort: and sort:- are allowed
'': compareSourceId_,
'active': compareActive_,
'desc': compareDescription_,
'description': compareDescription_,
'duration': compareDuration_,
'id': compareSourceId_,
'source': compareSourceType_,
'type': compareSourceType_
};
/**
* Sorts active entries first. If both entries are inactive, puts the one
* that was active most recently first. If both are active, uses source ID,
* which puts longer lived events at the top, and behaves better than using
* duration or time of first event.
*/
function compareActive_(source1, source2) {
if (!source1.isInactive() && source2.isInactive())
return -1;
if (source1.isInactive() && !source2.isInactive())
return 1;
if (source1.isInactive()) {
var deltaEndTime = source1.getEndTicks() - source2.getEndTicks();
if (deltaEndTime != 0) {
// The one that ended most recently (Highest end time) should be sorted
// first.
return -deltaEndTime;
}
// If both ended at the same time, then odds are they were related events,
// started one after another, so sort in the opposite order of their
// source IDs to get a more intuitive ordering.
return -compareSourceId_(source1, source2);
}
return compareSourceId_(source1, source2);
}
function compareDescription_(source1, source2) {
var source1Text = source1.getDescription().toLowerCase();
var source2Text = source2.getDescription().toLowerCase();
var compareResult = source1Text.localeCompare(source2Text);
if (compareResult != 0)
return compareResult;
return compareSourceId_(source1, source2);
}
function compareDuration_(source1, source2) {
var durationDifference = source2.getDuration() - source1.getDuration();
if (durationDifference)
return durationDifference;
return compareSourceId_(source1, source2);
}
/**
* For the purposes of sorting by source IDs, entries without a source
* appear right after the SourceEntry with the highest source ID received
* before the sourceless entry. Any ambiguities are resolved by ordering
* the entries without a source by the order in which they were received.
*/
function compareSourceId_(source1, source2) {
var sourceId1 = source1.getSourceId();
if (sourceId1 < 0)
sourceId1 = source1.getMaxPreviousEntrySourceId();
var sourceId2 = source2.getSourceId();
if (sourceId2 < 0)
sourceId2 = source2.getMaxPreviousEntrySourceId();
if (sourceId1 != sourceId2)
return sourceId1 - sourceId2;
// One or both have a negative ID. In either case, the source with the
// highest ID should be sorted first.
return source2.getSourceId() - source1.getSourceId();
}
function compareSourceType_(source1, source2) {
var source1Text = source1.getSourceTypeString();
var source2Text = source2.getSourceTypeString();
var compareResult = source1Text.localeCompare(source2Text);
if (compareResult != 0)
return compareResult;
return compareSourceId_(source1, source2);
}
return EventsView;
})();