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; |
+})(); |
+ |