| // Copyright (c) 2010 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. |
| |
| /** |
| * Each row in the filtered items list is backed by a SourceEntry. This |
| * instance contains all of the data pertaining to that row, and notifies |
| * its parent view (the EventsView) whenever its data changes. |
| * |
| * @constructor |
| */ |
| function SourceEntry(parentView, maxPreviousSourceId) { |
| this.maxPreviousSourceId_ = maxPreviousSourceId; |
| this.entries_ = []; |
| this.parentView_ = parentView; |
| this.isSelected_ = false; |
| this.isMatchedByFilter_ = false; |
| // If the first entry is a BEGIN_PHASE, set to true. |
| // Set to false when an END_PHASE matching the first entry is encountered. |
| this.isActive_ = false; |
| } |
| |
| SourceEntry.prototype.isSelected = function() { |
| return this.isSelected_; |
| }; |
| |
| SourceEntry.prototype.setSelectedStyles = function(isSelected) { |
| changeClassName(this.row_, 'selected', isSelected); |
| this.getSelectionCheckbox().checked = isSelected; |
| }; |
| |
| SourceEntry.prototype.setMouseoverStyle = function(isMouseOver) { |
| changeClassName(this.row_, 'mouseover', isMouseOver); |
| }; |
| |
| SourceEntry.prototype.setIsMatchedByFilter = function(isMatchedByFilter) { |
| if (this.isMatchedByFilter() == isMatchedByFilter) |
| return; // No change. |
| |
| this.isMatchedByFilter_ = isMatchedByFilter; |
| |
| this.setFilterStyles(isMatchedByFilter); |
| |
| if (isMatchedByFilter) { |
| this.parentView_.incrementPostfilterCount(1); |
| } else { |
| this.parentView_.incrementPostfilterCount(-1); |
| // If we are filtering an entry away, make sure it is no longer |
| // part of the selection. |
| this.setSelected(false); |
| } |
| }; |
| |
| SourceEntry.prototype.isMatchedByFilter = function() { |
| return this.isMatchedByFilter_; |
| }; |
| |
| SourceEntry.prototype.setFilterStyles = function(isMatchedByFilter) { |
| // Hide rows which have been filtered away. |
| if (isMatchedByFilter) { |
| this.row_.style.display = ''; |
| } else { |
| this.row_.style.display = 'none'; |
| } |
| }; |
| |
| SourceEntry.prototype.update = function(logEntry) { |
| if (logEntry.phase == LogEventPhase.PHASE_BEGIN && |
| this.entries_.length == 0) |
| this.isActive_ = true; |
| |
| // Only the last event should have the same type first event, |
| if (this.isActive_ && |
| logEntry.phase == LogEventPhase.PHASE_END && |
| logEntry.type == this.entries_[0].type) |
| this.isActive_ = false; |
| |
| var prevStartEntry = this.getStartEntry_(); |
| this.entries_.push(logEntry); |
| var curStartEntry = this.getStartEntry_(); |
| |
| // If we just got the first entry for this source. |
| if (prevStartEntry != curStartEntry) { |
| if (!prevStartEntry) |
| this.createRow_(); |
| else |
| this.updateDescription_(); |
| } |
| |
| // Update filters. |
| var matchesFilter = this.matchesFilter(this.parentView_.currentFilter_); |
| this.setIsMatchedByFilter(matchesFilter); |
| }; |
| |
| SourceEntry.prototype.onCheckboxToggled_ = function() { |
| this.setSelected(this.getSelectionCheckbox().checked); |
| }; |
| |
| SourceEntry.prototype.matchesFilter = function(filter) { |
| // Safety check. |
| if (this.row_ == null) |
| return false; |
| |
| if (filter.isActive && !this.isActive_) |
| return false; |
| if (filter.isInactive && this.isActive_) |
| return false; |
| |
| // Check source type, if needed. |
| if (filter.type) { |
| var sourceType = this.getSourceTypeString().toLowerCase(); |
| if (filter.type.indexOf(sourceType) == -1) |
| return false; |
| } |
| |
| // Check source ID, if needed. |
| if (filter.id) { |
| if (filter.id.indexOf(this.getSourceId() + '') == -1) |
| return false; |
| } |
| |
| if (filter.text == '') |
| return true; |
| |
| var filterText = filter.text; |
| var entryText = PrintSourceEntriesAsText(this.entries_).toLowerCase(); |
| |
| return entryText.indexOf(filterText) != -1; |
| }; |
| |
| SourceEntry.prototype.setSelected = function(isSelected) { |
| if (isSelected == this.isSelected()) |
| return; |
| |
| this.isSelected_ = isSelected; |
| |
| this.setSelectedStyles(isSelected); |
| this.parentView_.modifySelectionArray(this, isSelected); |
| this.parentView_.onSelectionChanged(); |
| }; |
| |
| SourceEntry.prototype.onClicked_ = function() { |
| this.parentView_.clearSelection(); |
| this.setSelected(true); |
| }; |
| |
| SourceEntry.prototype.onMouseover_ = function() { |
| this.setMouseoverStyle(true); |
| }; |
| |
| SourceEntry.prototype.onMouseout_ = function() { |
| this.setMouseoverStyle(false); |
| }; |
| |
| SourceEntry.prototype.updateDescription_ = function() { |
| this.descriptionCell_.innerHTML = ''; |
| addTextNode(this.descriptionCell_, this.getDescription()); |
| }; |
| |
| SourceEntry.prototype.createRow_ = function() { |
| // Create a row. |
| var tr = addNode(this.parentView_.tableBody_, 'tr'); |
| tr._id = this.getSourceId(); |
| tr.style.display = 'none'; |
| this.row_ = tr; |
| |
| var selectionCol = addNode(tr, 'td'); |
| var checkbox = addNode(selectionCol, 'input'); |
| checkbox.type = 'checkbox'; |
| |
| var idCell = addNode(tr, 'td'); |
| idCell.style.textAlign = 'right'; |
| |
| var typeCell = addNode(tr, 'td'); |
| var descriptionCell = addNode(tr, 'td'); |
| this.descriptionCell_ = descriptionCell; |
| |
| // Connect listeners. |
| checkbox.onchange = this.onCheckboxToggled_.bind(this); |
| |
| var onclick = this.onClicked_.bind(this); |
| idCell.onclick = onclick; |
| typeCell.onclick = onclick; |
| descriptionCell.onclick = onclick; |
| |
| tr.onmouseover = this.onMouseover_.bind(this); |
| tr.onmouseout = this.onMouseout_.bind(this); |
| |
| // Set the cell values to match this source's data. |
| if (this.getSourceId() >= 0) |
| addTextNode(idCell, this.getSourceId()); |
| else |
| addTextNode(idCell, '-'); |
| var sourceTypeString = this.getSourceTypeString(); |
| addTextNode(typeCell, sourceTypeString); |
| this.updateDescription_(); |
| |
| // Add a CSS classname specific to this source type (so CSS can specify |
| // different stylings for different types). |
| changeClassName(this.row_, 'source_' + sourceTypeString, true); |
| }; |
| |
| /** |
| * Returns a description for this source log stream, which will be displayed |
| * in the list view. Most often this is a URL that identifies the request, |
| * or a hostname for a connect job, etc... |
| */ |
| SourceEntry.prototype.getDescription = function() { |
| var e = this.getStartEntry_(); |
| if (!e) |
| return ''; |
| |
| if (e.source.type == LogSourceType.NONE) { |
| // NONE is what we use for global events that aren't actually grouped |
| // by a "source ID", so we will just stringize the event's type. |
| return getKeyWithValue(LogEventType, e.type); |
| } |
| |
| if (e.params == undefined) |
| return ''; |
| |
| var description = ''; |
| switch (e.source.type) { |
| case LogSourceType.URL_REQUEST: |
| case LogSourceType.SOCKET_STREAM: |
| case LogSourceType.HTTP_STREAM_JOB: |
| description = e.params.url; |
| break; |
| case LogSourceType.CONNECT_JOB: |
| description = e.params.group_name; |
| break; |
| case LogSourceType.HOST_RESOLVER_IMPL_REQUEST: |
| case LogSourceType.HOST_RESOLVER_IMPL_JOB: |
| description = e.params.host; |
| break; |
| case LogSourceType.DISK_CACHE_ENTRY: |
| case LogSourceType.MEMORY_CACHE_ENTRY: |
| description = e.params.key; |
| break; |
| case LogSourceType.SPDY_SESSION: |
| if (e.params.host) |
| description = e.params.host + ' (' + e.params.proxy + ')'; |
| break; |
| case LogSourceType.SOCKET: |
| if (e.params.source_dependency != undefined) { |
| var connectJobSourceEntry = |
| this.parentView_.getSourceEntry(e.params.source_dependency.id); |
| if (connectJobSourceEntry) |
| description = connectJobSourceEntry.getDescription(); |
| } |
| break; |
| } |
| |
| if (description == undefined) |
| return ''; |
| return description; |
| }; |
| |
| /** |
| * Returns the starting entry for this source. Conceptually this is the |
| * first entry that was logged to this source. However, we skip over the |
| * TYPE_REQUEST_ALIVE entries which wrap TYPE_URL_REQUEST_START_JOB / |
| * TYPE_SOCKET_STREAM_CONNECT. |
| */ |
| SourceEntry.prototype.getStartEntry_ = function() { |
| if (this.entries_.length < 1) |
| return undefined; |
| if (this.entries_.length >= 2) { |
| if (this.entries_[0].type == LogEventType.REQUEST_ALIVE || |
| this.entries_[0].type == LogEventType.SOCKET_POOL_CONNECT_JOB) |
| return this.entries_[1]; |
| } |
| return this.entries_[0]; |
| }; |
| |
| SourceEntry.prototype.getLogEntries = function() { |
| return this.entries_; |
| }; |
| |
| SourceEntry.prototype.getSourceTypeString = function() { |
| return getKeyWithValue(LogSourceType, this.entries_[0].source.type); |
| }; |
| |
| SourceEntry.prototype.getSelectionCheckbox = function() { |
| return this.row_.childNodes[0].firstChild; |
| }; |
| |
| SourceEntry.prototype.getSourceId = function() { |
| return this.entries_[0].source.id; |
| }; |
| |
| /** |
| * Returns the largest source ID seen before this object was received. |
| * Used only for sorting SourceEntries without a source by source ID. |
| */ |
| SourceEntry.prototype.getMaxPreviousEntrySourceId = function() { |
| return this.maxPreviousSourceId_; |
| }; |
| |
| SourceEntry.prototype.isActive = function() { |
| return this.isActive_; |
| }; |
| |
| /** |
| * Returns time of last event if inactive. Returns current time otherwise. |
| */ |
| SourceEntry.prototype.getEndTime = function() { |
| if (this.isActive_) { |
| return (new Date()).getTime(); |
| } |
| else { |
| var endTicks = this.entries_[this.entries_.length - 1].time; |
| return g_browser.convertTimeTicksToDate(endTicks).getTime(); |
| } |
| }; |
| |
| /** |
| * Returns the time between the first and last events with a matching |
| * source ID. If source is still active, uses the current time for the |
| * last event. |
| */ |
| SourceEntry.prototype.getDuration = function() { |
| var startTicks = this.entries_[0].time; |
| var startTime = g_browser.convertTimeTicksToDate(startTicks).getTime(); |
| var endTime = this.getEndTime(); |
| return endTime - startTime; |
| }; |
| |
| /** |
| * Returns source ID of the entry whose row is currently above this one's. |
| * Returns null if no such node exists. |
| */ |
| SourceEntry.prototype.getPreviousNodeSourceId = function() { |
| if (!this.hasRow()) |
| return null; |
| var prevNode = this.row_.previousSibling; |
| if (prevNode == null) |
| return null; |
| return prevNode._id; |
| }; |
| |
| /** |
| * Returns source ID of the entry whose row is currently below this one's. |
| * Returns null if no such node exists. |
| */ |
| SourceEntry.prototype.getNextNodeSourceId = function() { |
| if (!this.hasRow()) |
| return null; |
| var nextNode = this.row_.nextSibling; |
| if (nextNode == null) |
| return null; |
| return nextNode._id; |
| }; |
| |
| SourceEntry.prototype.hasRow = function() { |
| return this.row_ != null; |
| }; |
| |
| /** |
| * Moves current object's row before |entry|'s row. |
| */ |
| SourceEntry.prototype.moveBefore = function(entry) { |
| if (this.hasRow() && entry.hasRow()) { |
| this.row_.parentNode.insertBefore(this.row_, entry.row_); |
| } |
| }; |
| |
| /** |
| * Moves current object's row after |entry|'s row. |
| */ |
| SourceEntry.prototype.moveAfter = function(entry) { |
| if (this.hasRow() && entry.hasRow()) { |
| this.row_.parentNode.insertBefore(this.row_, entry.row_.nextSibling); |
| } |
| }; |
| |
| SourceEntry.prototype.remove = function() { |
| this.setSelected(false); |
| this.setIsMatchedByFilter(false); |
| this.row_.parentNode.removeChild(this.row_); |
| }; |
| |