// 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_);
};