| Index: netlog_viewer/events_view.js
|
| diff --git a/netlog_viewer/events_view.js b/netlog_viewer/events_view.js
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..49e06221abaf3ad0ae5ab8fe4620752bd65e85de
|
| --- /dev/null
|
| +++ b/netlog_viewer/events_view.js
|
| @@ -0,0 +1,579 @@
|
| +// 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;
|
| +})();
|
| +
|
|
|