1// Copyright (c) 2010 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/**
6 * Each row in the filtered items list is backed by a SourceEntry. This
7 * instance contains all of the data pertaining to that row, and notifies
8 * its parent view (the EventsView) whenever its data changes.
9 *
10 * @constructor
11 */
12function SourceEntry(parentView, maxPreviousSourceId) {
13  this.maxPreviousSourceId_ = maxPreviousSourceId;
14  this.entries_ = [];
15  this.parentView_ = parentView;
16  this.isSelected_ = false;
17  this.isMatchedByFilter_ = false;
18  // If the first entry is a BEGIN_PHASE, set to true.
19  // Set to false when an END_PHASE matching the first entry is encountered.
20  this.isActive_ = false;
21}
22
23SourceEntry.prototype.isSelected = function() {
24  return this.isSelected_;
25};
26
27SourceEntry.prototype.setSelectedStyles = function(isSelected) {
28  changeClassName(this.row_, 'selected', isSelected);
29  this.getSelectionCheckbox().checked = isSelected;
30};
31
32SourceEntry.prototype.setMouseoverStyle = function(isMouseOver) {
33  changeClassName(this.row_, 'mouseover', isMouseOver);
34};
35
36SourceEntry.prototype.setIsMatchedByFilter = function(isMatchedByFilter) {
37  if (this.isMatchedByFilter() == isMatchedByFilter)
38    return;  // No change.
39
40  this.isMatchedByFilter_ = isMatchedByFilter;
41
42  this.setFilterStyles(isMatchedByFilter);
43
44  if (isMatchedByFilter) {
45    this.parentView_.incrementPostfilterCount(1);
46  } else {
47    this.parentView_.incrementPostfilterCount(-1);
48    // If we are filtering an entry away, make sure it is no longer
49    // part of the selection.
50    this.setSelected(false);
51  }
52};
53
54SourceEntry.prototype.isMatchedByFilter = function() {
55  return this.isMatchedByFilter_;
56};
57
58SourceEntry.prototype.setFilterStyles = function(isMatchedByFilter) {
59  // Hide rows which have been filtered away.
60  if (isMatchedByFilter) {
61    this.row_.style.display = '';
62  } else {
63    this.row_.style.display = 'none';
64  }
65};
66
67SourceEntry.prototype.update = function(logEntry) {
68  if (logEntry.phase == LogEventPhase.PHASE_BEGIN &&
69      this.entries_.length == 0)
70    this.isActive_ = true;
71
72  // Only the last event should have the same type first event,
73  if (this.isActive_ &&
74      logEntry.phase == LogEventPhase.PHASE_END &&
75      logEntry.type == this.entries_[0].type)
76    this.isActive_ = false;
77
78  var prevStartEntry = this.getStartEntry_();
79  this.entries_.push(logEntry);
80  var curStartEntry = this.getStartEntry_();
81
82  // If we just got the first entry for this source.
83  if (prevStartEntry != curStartEntry) {
84    if (!prevStartEntry)
85      this.createRow_();
86    else
87      this.updateDescription_();
88  }
89
90  // Update filters.
91  var matchesFilter = this.matchesFilter(this.parentView_.currentFilter_);
92  this.setIsMatchedByFilter(matchesFilter);
93};
94
95SourceEntry.prototype.onCheckboxToggled_ = function() {
96  this.setSelected(this.getSelectionCheckbox().checked);
97};
98
99SourceEntry.prototype.matchesFilter = function(filter) {
100  // Safety check.
101  if (this.row_ == null)
102    return false;
103
104  if (filter.isActive && !this.isActive_)
105    return false;
106  if (filter.isInactive && this.isActive_)
107    return false;
108
109  // Check source type, if needed.
110  if (filter.type) {
111    var sourceType = this.getSourceTypeString().toLowerCase();
112    if (filter.type.indexOf(sourceType) == -1)
113      return false;
114  }
115
116  // Check source ID, if needed.
117  if (filter.id) {
118    if (filter.id.indexOf(this.getSourceId() + '') == -1)
119      return false;
120  }
121
122  if (filter.text == '')
123    return true;
124
125  var filterText = filter.text;
126  var entryText = PrintSourceEntriesAsText(this.entries_).toLowerCase();
127
128  return entryText.indexOf(filterText) != -1;
129};
130
131SourceEntry.prototype.setSelected = function(isSelected) {
132  if (isSelected == this.isSelected())
133    return;
134
135  this.isSelected_ = isSelected;
136
137  this.setSelectedStyles(isSelected);
138  this.parentView_.modifySelectionArray(this, isSelected);
139  this.parentView_.onSelectionChanged();
140};
141
142SourceEntry.prototype.onClicked_ = function() {
143  this.parentView_.clearSelection();
144  this.setSelected(true);
145};
146
147SourceEntry.prototype.onMouseover_ = function() {
148  this.setMouseoverStyle(true);
149};
150
151SourceEntry.prototype.onMouseout_ = function() {
152  this.setMouseoverStyle(false);
153};
154
155SourceEntry.prototype.updateDescription_ = function() {
156  this.descriptionCell_.innerHTML = '';
157  addTextNode(this.descriptionCell_, this.getDescription());
158};
159
160SourceEntry.prototype.createRow_ = function() {
161  // Create a row.
162  var tr = addNode(this.parentView_.tableBody_, 'tr');
163  tr._id = this.getSourceId();
164  tr.style.display = 'none';
165  this.row_ = tr;
166
167  var selectionCol = addNode(tr, 'td');
168  var checkbox = addNode(selectionCol, 'input');
169  checkbox.type = 'checkbox';
170
171  var idCell = addNode(tr, 'td');
172  idCell.style.textAlign = 'right';
173
174  var typeCell = addNode(tr, 'td');
175  var descriptionCell = addNode(tr, 'td');
176  this.descriptionCell_ = descriptionCell;
177
178  // Connect listeners.
179  checkbox.onchange = this.onCheckboxToggled_.bind(this);
180
181  var onclick = this.onClicked_.bind(this);
182  idCell.onclick = onclick;
183  typeCell.onclick = onclick;
184  descriptionCell.onclick = onclick;
185
186  tr.onmouseover = this.onMouseover_.bind(this);
187  tr.onmouseout = this.onMouseout_.bind(this);
188
189  // Set the cell values to match this source's data.
190  if (this.getSourceId() >= 0)
191    addTextNode(idCell, this.getSourceId());
192  else
193    addTextNode(idCell, '-');
194  var sourceTypeString = this.getSourceTypeString();
195  addTextNode(typeCell, sourceTypeString);
196  this.updateDescription_();
197
198  // Add a CSS classname specific to this source type (so CSS can specify
199  // different stylings for different types).
200  changeClassName(this.row_, 'source_' + sourceTypeString, true);
201};
202
203/**
204 * Returns a description for this source log stream, which will be displayed
205 * in the list view. Most often this is a URL that identifies the request,
206 * or a hostname for a connect job, etc...
207 */
208SourceEntry.prototype.getDescription = function() {
209  var e = this.getStartEntry_();
210  if (!e)
211    return '';
212
213  if (e.source.type == LogSourceType.NONE) {
214    // NONE is what we use for global events that aren't actually grouped
215    // by a "source ID", so we will just stringize the event's type.
216    return getKeyWithValue(LogEventType, e.type);
217  }
218
219  if (e.params == undefined)
220    return '';
221
222  var description = '';
223  switch (e.source.type) {
224    case LogSourceType.URL_REQUEST:
225    case LogSourceType.SOCKET_STREAM:
226    case LogSourceType.HTTP_STREAM_JOB:
227      description = e.params.url;
228      break;
229    case LogSourceType.CONNECT_JOB:
230      description = e.params.group_name;
231      break;
232    case LogSourceType.HOST_RESOLVER_IMPL_REQUEST:
233    case LogSourceType.HOST_RESOLVER_IMPL_JOB:
234      description = e.params.host;
235      break;
236    case LogSourceType.DISK_CACHE_ENTRY:
237    case LogSourceType.MEMORY_CACHE_ENTRY:
238      description = e.params.key;
239      break;
240    case LogSourceType.SPDY_SESSION:
241      if (e.params.host)
242        description = e.params.host + ' (' + e.params.proxy + ')';
243      break;
244    case LogSourceType.SOCKET:
245      if (e.params.source_dependency != undefined) {
246        var connectJobSourceEntry =
247            this.parentView_.getSourceEntry(e.params.source_dependency.id);
248        if (connectJobSourceEntry)
249          description = connectJobSourceEntry.getDescription();
250      }
251      break;
252  }
253
254  if (description == undefined)
255    return '';
256  return description;
257};
258
259/**
260 * Returns the starting entry for this source. Conceptually this is the
261 * first entry that was logged to this source. However, we skip over the
262 * TYPE_REQUEST_ALIVE entries which wrap TYPE_URL_REQUEST_START_JOB /
263 * TYPE_SOCKET_STREAM_CONNECT.
264 */
265SourceEntry.prototype.getStartEntry_ = function() {
266  if (this.entries_.length < 1)
267    return undefined;
268  if (this.entries_.length >= 2) {
269    if (this.entries_[0].type == LogEventType.REQUEST_ALIVE ||
270        this.entries_[0].type == LogEventType.SOCKET_POOL_CONNECT_JOB)
271      return this.entries_[1];
272  }
273  return this.entries_[0];
274};
275
276SourceEntry.prototype.getLogEntries = function() {
277  return this.entries_;
278};
279
280SourceEntry.prototype.getSourceTypeString = function() {
281  return getKeyWithValue(LogSourceType, this.entries_[0].source.type);
282};
283
284SourceEntry.prototype.getSelectionCheckbox = function() {
285  return this.row_.childNodes[0].firstChild;
286};
287
288SourceEntry.prototype.getSourceId = function() {
289  return this.entries_[0].source.id;
290};
291
292/**
293 * Returns the largest source ID seen before this object was received.
294 * Used only for sorting SourceEntries without a source by source ID.
295 */
296SourceEntry.prototype.getMaxPreviousEntrySourceId = function() {
297  return this.maxPreviousSourceId_;
298};
299
300SourceEntry.prototype.isActive = function() {
301  return this.isActive_;
302};
303
304/**
305 * Returns time of last event if inactive.  Returns current time otherwise.
306 */
307SourceEntry.prototype.getEndTime = function() {
308  if (this.isActive_) {
309    return (new Date()).getTime();
310  }
311  else {
312    var endTicks = this.entries_[this.entries_.length - 1].time;
313    return g_browser.convertTimeTicksToDate(endTicks).getTime();
314  }
315};
316
317/**
318 * Returns the time between the first and last events with a matching
319 * source ID.  If source is still active, uses the current time for the
320 * last event.
321 */
322SourceEntry.prototype.getDuration = function() {
323  var startTicks = this.entries_[0].time;
324  var startTime = g_browser.convertTimeTicksToDate(startTicks).getTime();
325  var endTime = this.getEndTime();
326  return endTime - startTime;
327};
328
329/**
330 * Returns source ID of the entry whose row is currently above this one's.
331 * Returns null if no such node exists.
332 */
333SourceEntry.prototype.getPreviousNodeSourceId = function() {
334  if (!this.hasRow())
335    return null;
336  var prevNode = this.row_.previousSibling;
337  if (prevNode == null)
338    return null;
339  return prevNode._id;
340};
341
342/**
343 * Returns source ID of the entry whose row is currently below this one's.
344 * Returns null if no such node exists.
345 */
346SourceEntry.prototype.getNextNodeSourceId = function() {
347  if (!this.hasRow())
348    return null;
349  var nextNode = this.row_.nextSibling;
350  if (nextNode == null)
351    return null;
352  return nextNode._id;
353};
354
355SourceEntry.prototype.hasRow = function() {
356  return this.row_ != null;
357};
358
359/**
360 * Moves current object's row before |entry|'s row.
361 */
362SourceEntry.prototype.moveBefore = function(entry) {
363  if (this.hasRow() && entry.hasRow()) {
364    this.row_.parentNode.insertBefore(this.row_, entry.row_);
365  }
366};
367
368/**
369 * Moves current object's row after |entry|'s row.
370 */
371SourceEntry.prototype.moveAfter = function(entry) {
372  if (this.hasRow() && entry.hasRow()) {
373    this.row_.parentNode.insertBefore(this.row_, entry.row_.nextSibling);
374  }
375};
376
377SourceEntry.prototype.remove = function() {
378  this.setSelected(false);
379  this.setIsMatchedByFilter(false);
380  this.row_.parentNode.removeChild(this.row_);
381};
382
383