1// Copyright (c) 2011 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 * EventsView displays a filtered list of all events sharing a source, and 7 * a details pane for the selected sources. 8 * 9 * +----------------------++----------------+ 10 * | filter box || | 11 * +----------------------+| | 12 * | || | 13 * | || | 14 * | || | 15 * | || | 16 * | source list || details | 17 * | || view | 18 * | || | 19 * | || | 20 * | || | 21 * | || | 22 * +----------------------++ | 23 * | action bar || | 24 * +----------------------++----------------+ 25 * 26 * @constructor 27 */ 28function EventsView(tableBodyId, filterInputId, filterCountId, 29 deleteSelectedId, deleteAllId, selectAllId, sortByIdId, 30 sortBySourceTypeId, sortByDescriptionId, 31 tabHandlesContainerId, logTabId, timelineTabId, 32 detailsLogBoxId, detailsTimelineBoxId, 33 topbarId, middleboxId, bottombarId, sizerId) { 34 View.call(this); 35 36 // Used for sorting entries with automatically assigned IDs. 37 this.maxReceivedSourceId_ = 0; 38 39 // Initialize the sub-views. 40 var leftPane = new TopMidBottomView(new DivView(topbarId), 41 new DivView(middleboxId), 42 new DivView(bottombarId)); 43 44 this.detailsView_ = new DetailsView(tabHandlesContainerId, 45 logTabId, 46 timelineTabId, 47 detailsLogBoxId, 48 detailsTimelineBoxId); 49 50 this.splitterView_ = new ResizableVerticalSplitView( 51 leftPane, this.detailsView_, new DivView(sizerId)); 52 53 g_browser.addLogObserver(this); 54 55 this.tableBody_ = document.getElementById(tableBodyId); 56 57 this.filterInput_ = document.getElementById(filterInputId); 58 this.filterCount_ = document.getElementById(filterCountId); 59 60 this.filterInput_.addEventListener('search', 61 this.onFilterTextChanged_.bind(this), true); 62 63 document.getElementById(deleteSelectedId).onclick = 64 this.deleteSelected_.bind(this); 65 66 document.getElementById(deleteAllId).onclick = 67 g_browser.deleteAllEvents.bind(g_browser); 68 69 document.getElementById(selectAllId).addEventListener( 70 'click', this.selectAll_.bind(this), true); 71 72 document.getElementById(sortByIdId).addEventListener( 73 'click', this.sortById_.bind(this), true); 74 75 document.getElementById(sortBySourceTypeId).addEventListener( 76 'click', this.sortBySourceType_.bind(this), true); 77 78 document.getElementById(sortByDescriptionId).addEventListener( 79 'click', this.sortByDescription_.bind(this), true); 80 81 // Sets sort order and filter. 82 this.setFilter_(''); 83 84 this.initializeSourceList_(); 85} 86 87inherits(EventsView, View); 88 89/** 90 * Initializes the list of source entries. If source entries are already, 91 * being displayed, removes them all in the process. 92 */ 93EventsView.prototype.initializeSourceList_ = function() { 94 this.currentSelectedSources_ = []; 95 this.sourceIdToEntryMap_ = {}; 96 this.tableBody_.innerHTML = ''; 97 this.numPrefilter_ = 0; 98 this.numPostfilter_ = 0; 99 this.invalidateFilterCounter_(); 100 this.invalidateDetailsView_(); 101}; 102 103// How soon after updating the filter list the counter should be updated. 104EventsView.REPAINT_FILTER_COUNTER_TIMEOUT_MS = 0; 105 106EventsView.prototype.setGeometry = function(left, top, width, height) { 107 EventsView.superClass_.setGeometry.call(this, left, top, width, height); 108 this.splitterView_.setGeometry(left, top, width, height); 109}; 110 111EventsView.prototype.show = function(isVisible) { 112 EventsView.superClass_.show.call(this, isVisible); 113 this.splitterView_.show(isVisible); 114}; 115 116EventsView.prototype.getFilterText_ = function() { 117 return this.filterInput_.value; 118}; 119 120EventsView.prototype.setFilterText_ = function(filterText) { 121 this.filterInput_.value = filterText; 122 this.onFilterTextChanged_(); 123}; 124 125EventsView.prototype.onFilterTextChanged_ = function() { 126 this.setFilter_(this.getFilterText_()); 127}; 128 129/** 130 * Updates text in the details view when security stripping is toggled. 131 */ 132EventsView.prototype.onSecurityStrippingChanged = function() { 133 this.invalidateDetailsView_(); 134} 135 136/** 137 * Sorts active entries first. If both entries are inactive, puts the one 138 * that was active most recently first. If both are active, uses source ID, 139 * which puts longer lived events at the top, and behaves better than using 140 * duration or time of first event. 141 */ 142EventsView.compareActive_ = function(source1, source2) { 143 if (source1.isActive() && !source2.isActive()) 144 return -1; 145 if (!source1.isActive() && source2.isActive()) 146 return 1; 147 if (!source1.isActive()) { 148 var deltaEndTime = source1.getEndTime() - source2.getEndTime(); 149 if (deltaEndTime != 0) { 150 // The one that ended most recently (Highest end time) should be sorted 151 // first. 152 return -deltaEndTime; 153 } 154 // If both ended at the same time, then odds are they were related events, 155 // started one after another, so sort in the opposite order of their 156 // source IDs to get a more intuitive ordering. 157 return -EventsView.compareSourceId_(source1, source2); 158 } 159 return EventsView.compareSourceId_(source1, source2); 160}; 161 162EventsView.compareDescription_ = function(source1, source2) { 163 var source1Text = source1.getDescription().toLowerCase(); 164 var source2Text = source2.getDescription().toLowerCase(); 165 var compareResult = source1Text.localeCompare(source2Text); 166 if (compareResult != 0) 167 return compareResult; 168 return EventsView.compareSourceId_(source1, source2); 169}; 170 171EventsView.compareDuration_ = function(source1, source2) { 172 var durationDifference = source2.getDuration() - source1.getDuration(); 173 if (durationDifference) 174 return durationDifference; 175 return EventsView.compareSourceId_(source1, source2); 176}; 177 178/** 179 * For the purposes of sorting by source IDs, entries without a source 180 * appear right after the SourceEntry with the highest source ID received 181 * before the sourceless entry. Any ambiguities are resolved by ordering 182 * the entries without a source by the order in which they were received. 183 */ 184EventsView.compareSourceId_ = function(source1, source2) { 185 var sourceId1 = source1.getSourceId(); 186 if (sourceId1 < 0) 187 sourceId1 = source1.getMaxPreviousEntrySourceId(); 188 var sourceId2 = source2.getSourceId(); 189 if (sourceId2 < 0) 190 sourceId2 = source2.getMaxPreviousEntrySourceId(); 191 192 if (sourceId1 != sourceId2) 193 return sourceId1 - sourceId2; 194 195 // One or both have a negative ID. In either case, the source with the 196 // highest ID should be sorted first. 197 return source2.getSourceId() - source1.getSourceId(); 198}; 199 200EventsView.compareSourceType_ = function(source1, source2) { 201 var source1Text = source1.getSourceTypeString(); 202 var source2Text = source2.getSourceTypeString(); 203 var compareResult = source1Text.localeCompare(source2Text); 204 if (compareResult != 0) 205 return compareResult; 206 return EventsView.compareSourceId_(source1, source2); 207}; 208 209EventsView.prototype.comparisonFuncWithReversing_ = function(a, b) { 210 var result = this.comparisonFunction_(a, b); 211 if (this.doSortBackwards_) 212 result *= -1; 213 return result; 214}; 215 216EventsView.comparisonFunctionTable_ = { 217 // sort: and sort:- are allowed 218 '': EventsView.compareSourceId_, 219 'active': EventsView.compareActive_, 220 'desc': EventsView.compareDescription_, 221 'description': EventsView.compareDescription_, 222 'duration': EventsView.compareDuration_, 223 'id': EventsView.compareSourceId_, 224 'source': EventsView.compareSourceType_, 225 'type': EventsView.compareSourceType_ 226}; 227 228EventsView.prototype.Sort_ = function() { 229 var sourceEntries = []; 230 for (var id in this.sourceIdToEntryMap_) { 231 // Can only sort items with an actual row in the table. 232 if (this.sourceIdToEntryMap_[id].hasRow()) 233 sourceEntries.push(this.sourceIdToEntryMap_[id]); 234 } 235 sourceEntries.sort(this.comparisonFuncWithReversing_.bind(this)); 236 237 for (var i = sourceEntries.length - 2; i >= 0; --i) { 238 if (sourceEntries[i].getNextNodeSourceId() != 239 sourceEntries[i + 1].getSourceId()) 240 sourceEntries[i].moveBefore(sourceEntries[i + 1]); 241 } 242}; 243 244/** 245 * Looks for the first occurence of |directive|:parameter in |sourceText|. 246 * Parameter can be an empty string. 247 * 248 * On success, returns an object with two fields: 249 * |remainingText| - |sourceText| with |directive|:parameter removed, 250 and excess whitespace deleted. 251 * |parameter| - the parameter itself. 252 * 253 * On failure, returns null. 254 */ 255EventsView.prototype.parseDirective_ = function(sourceText, directive) { 256 // Adding a leading space allows a single regexp to be used, regardless of 257 // whether or not the directive is at the start of the string. 258 sourceText = ' ' + sourceText; 259 regExp = new RegExp('\\s+' + directive + ':(\\S*)\\s*', 'i'); 260 matchInfo = regExp.exec(sourceText); 261 if (matchInfo == null) 262 return null; 263 264 return {'remainingText': sourceText.replace(regExp, ' ').trim(), 265 'parameter': matchInfo[1]}; 266}; 267 268/** 269 * Just like parseDirective_, except can optionally be a '-' before or 270 * the parameter, to negate it. Before is more natural, after 271 * allows more convenient toggling. 272 * 273 * Returned value has the additional field |isNegated|, and a leading 274 * '-' will be removed from |parameter|, if present. 275 */ 276EventsView.prototype.parseNegatableDirective_ = function(sourceText, 277 directive) { 278 var matchInfo = this.parseDirective_(sourceText, directive); 279 if (matchInfo == null) 280 return null; 281 282 // Remove any leading or trailing '-' from the directive. 283 var negationInfo = /^(-?)(\S*?)$/.exec(matchInfo.parameter); 284 matchInfo.parameter = negationInfo[2]; 285 matchInfo.isNegated = (negationInfo[1] == '-'); 286 return matchInfo; 287}; 288 289/** 290 * Parse any "sort:" directives, and update |comparisonFunction_| and 291 * |doSortBackwards_|as needed. Note only the last valid sort directive 292 * is used. 293 * 294 * Returns |filterText| with all sort directives removed, including 295 * invalid ones. 296 */ 297EventsView.prototype.parseSortDirectives_ = function(filterText) { 298 this.comparisonFunction_ = EventsView.compareSourceId_; 299 this.doSortBackwards_ = false; 300 301 while (true) { 302 var sortInfo = this.parseNegatableDirective_(filterText, 'sort'); 303 if (sortInfo == null) 304 break; 305 var comparisonName = sortInfo.parameter.toLowerCase(); 306 if (EventsView.comparisonFunctionTable_[comparisonName] != null) { 307 this.comparisonFunction_ = 308 EventsView.comparisonFunctionTable_[comparisonName]; 309 this.doSortBackwards_ = sortInfo.isNegated; 310 } 311 filterText = sortInfo.remainingText; 312 } 313 314 return filterText; 315}; 316 317/** 318 * Parse any "is:" directives, and update |filter| accordingly. 319 * 320 * Returns |filterText| with all "is:" directives removed, including 321 * invalid ones. 322 */ 323EventsView.prototype.parseRestrictDirectives_ = function(filterText, filter) { 324 while (true) { 325 var filterInfo = this.parseNegatableDirective_(filterText, 'is'); 326 if (filterInfo == null) 327 break; 328 if (filterInfo.parameter == 'active') { 329 if (!filterInfo.isNegated) 330 filter.isActive = true; 331 else 332 filter.isInactive = true; 333 } 334 filterText = filterInfo.remainingText; 335 } 336 return filterText; 337}; 338 339/** 340 * Parses all directives that take arbitrary strings as input, 341 * and updates |filter| accordingly. Directives of these types 342 * are stored as lists. 343 * 344 * Returns |filterText| with all recognized directives removed. 345 */ 346EventsView.prototype.parseStringDirectives_ = function(filterText, filter) { 347 var directives = ['type', 'id']; 348 for (var i = 0; i < directives.length; ++i) { 349 while (true) { 350 var directive = directives[i]; 351 var filterInfo = this.parseDirective_(filterText, directive); 352 if (filterInfo == null) 353 break; 354 if (!filter[directive]) 355 filter[directive] = []; 356 filter[directive].push(filterInfo.parameter); 357 filterText = filterInfo.remainingText; 358 } 359 } 360 return filterText; 361}; 362 363/* 364 * Converts |filterText| into an object representing the filter. 365 */ 366EventsView.prototype.createFilter_ = function(filterText) { 367 var filter = {}; 368 filterText = filterText.toLowerCase(); 369 filterText = this.parseRestrictDirectives_(filterText, filter); 370 filterText = this.parseStringDirectives_(filterText, filter); 371 filter.text = filterText.trim(); 372 return filter; 373}; 374 375EventsView.prototype.setFilter_ = function(filterText) { 376 var lastComparisonFunction = this.comparisonFunction_; 377 var lastDoSortBackwards = this.doSortBackwards_; 378 379 filterText = this.parseSortDirectives_(filterText); 380 381 if (lastComparisonFunction != this.comparisonFunction_ || 382 lastDoSortBackwards != this.doSortBackwards_) { 383 this.Sort_(); 384 } 385 386 this.currentFilter_ = this.createFilter_(filterText); 387 388 // Iterate through all of the rows and see if they match the filter. 389 for (var id in this.sourceIdToEntryMap_) { 390 var entry = this.sourceIdToEntryMap_[id]; 391 entry.setIsMatchedByFilter(entry.matchesFilter(this.currentFilter_)); 392 } 393}; 394 395/** 396 * Repositions |sourceEntry|'s row in the table using an insertion sort. 397 * Significantly faster than sorting the entire table again, when only 398 * one entry has changed. 399 */ 400EventsView.prototype.InsertionSort_ = function(sourceEntry) { 401 // SourceEntry that should be after |sourceEntry|, if it needs 402 // to be moved earlier in the list. 403 var sourceEntryAfter = sourceEntry; 404 while (true) { 405 var prevSourceId = sourceEntryAfter.getPreviousNodeSourceId(); 406 if (prevSourceId == null) 407 break; 408 var prevSourceEntry = this.sourceIdToEntryMap_[prevSourceId]; 409 if (this.comparisonFuncWithReversing_(sourceEntry, prevSourceEntry) >= 0) 410 break; 411 sourceEntryAfter = prevSourceEntry; 412 } 413 if (sourceEntryAfter != sourceEntry) { 414 sourceEntry.moveBefore(sourceEntryAfter); 415 return; 416 } 417 418 var sourceEntryBefore = sourceEntry; 419 while (true) { 420 var nextSourceId = sourceEntryBefore.getNextNodeSourceId(); 421 if (nextSourceId == null) 422 break; 423 var nextSourceEntry = this.sourceIdToEntryMap_[nextSourceId]; 424 if (this.comparisonFuncWithReversing_(sourceEntry, nextSourceEntry) <= 0) 425 break; 426 sourceEntryBefore = nextSourceEntry; 427 } 428 if (sourceEntryBefore != sourceEntry) 429 sourceEntry.moveAfter(sourceEntryBefore); 430}; 431 432EventsView.prototype.onLogEntryAdded = function(logEntry) { 433 var id = logEntry.source.id; 434 435 // Lookup the source. 436 var sourceEntry = this.sourceIdToEntryMap_[id]; 437 438 if (!sourceEntry) { 439 sourceEntry = new SourceEntry(this, this.maxReceivedSourceId_); 440 this.sourceIdToEntryMap_[id] = sourceEntry; 441 this.incrementPrefilterCount(1); 442 if (id > this.maxReceivedSourceId_) 443 this.maxReceivedSourceId_ = id; 444 } 445 446 sourceEntry.update(logEntry); 447 448 if (sourceEntry.isSelected()) 449 this.invalidateDetailsView_(); 450 451 // TODO(mmenke): Fix sorting when sorting by duration. 452 // Duration continuously increases for all entries that are 453 // still active. This can result in incorrect sorting, until 454 // Sort_ is called. 455 this.InsertionSort_(sourceEntry); 456}; 457 458/** 459 * Returns the SourceEntry with the specified ID, if there is one. 460 * Otherwise, returns undefined. 461 */ 462EventsView.prototype.getSourceEntry = function(id) { 463 return this.sourceIdToEntryMap_[id]; 464}; 465 466/** 467 * Called whenever some log events are deleted. |sourceIds| lists 468 * the source IDs of all deleted log entries. 469 */ 470EventsView.prototype.onLogEntriesDeleted = function(sourceIds) { 471 for (var i = 0; i < sourceIds.length; ++i) { 472 var id = sourceIds[i]; 473 var entry = this.sourceIdToEntryMap_[id]; 474 if (entry) { 475 entry.remove(); 476 delete this.sourceIdToEntryMap_[id]; 477 this.incrementPrefilterCount(-1); 478 } 479 } 480}; 481 482/** 483 * Called whenever all log events are deleted. 484 */ 485EventsView.prototype.onAllLogEntriesDeleted = function() { 486 this.initializeSourceList_(); 487}; 488 489/** 490 * Called when either a log file is loaded or when going back to actively 491 * logging events. In either case, called after clearing the old entries, 492 * but before getting any new ones. 493 */ 494EventsView.prototype.onSetIsViewingLogFile = function(isViewingLogFile) { 495 // Needed to sort new sourceless entries correctly. 496 this.maxReceivedSourceId_ = 0; 497}; 498 499EventsView.prototype.incrementPrefilterCount = function(offset) { 500 this.numPrefilter_ += offset; 501 this.invalidateFilterCounter_(); 502}; 503 504EventsView.prototype.incrementPostfilterCount = function(offset) { 505 this.numPostfilter_ += offset; 506 this.invalidateFilterCounter_(); 507}; 508 509EventsView.prototype.onSelectionChanged = function() { 510 this.invalidateDetailsView_(); 511}; 512 513EventsView.prototype.clearSelection = function() { 514 var prevSelection = this.currentSelectedSources_; 515 this.currentSelectedSources_ = []; 516 517 // Unselect everything that is currently selected. 518 for (var i = 0; i < prevSelection.length; ++i) { 519 prevSelection[i].setSelected(false); 520 } 521 522 this.onSelectionChanged(); 523}; 524 525EventsView.prototype.deleteSelected_ = function() { 526 var sourceIds = []; 527 for (var i = 0; i < this.currentSelectedSources_.length; ++i) { 528 var entry = this.currentSelectedSources_[i]; 529 sourceIds.push(entry.getSourceId()); 530 } 531 g_browser.deleteEventsBySourceId(sourceIds); 532}; 533 534EventsView.prototype.selectAll_ = function(event) { 535 for (var id in this.sourceIdToEntryMap_) { 536 var entry = this.sourceIdToEntryMap_[id]; 537 if (entry.isMatchedByFilter()) { 538 entry.setSelected(true); 539 } 540 } 541 event.preventDefault(); 542}; 543 544EventsView.prototype.unselectAll_ = function() { 545 var entries = this.currentSelectedSources_.slice(0); 546 for (var i = 0; i < entries.length; ++i) { 547 entries[i].setSelected(false); 548 } 549}; 550 551/** 552 * If |params| includes a query, replaces the current filter and unselects. 553 * all items. 554 */ 555EventsView.prototype.setParameters = function(params) { 556 if (params.q) { 557 this.unselectAll_(); 558 this.setFilterText_(params.q); 559 } 560}; 561 562/** 563 * If already using the specified sort method, flips direction. Otherwise, 564 * removes pre-existing sort parameter before adding the new one. 565 */ 566EventsView.prototype.toggleSortMethod_ = function(sortMethod) { 567 // Remove old sort directives, if any. 568 var filterText = this.parseSortDirectives_(this.getFilterText_()); 569 570 // If already using specified sortMethod, sort backwards. 571 if (!this.doSortBackwards_ && 572 EventsView.comparisonFunctionTable_[sortMethod] == 573 this.comparisonFunction_) 574 sortMethod = '-' + sortMethod; 575 576 filterText = 'sort:' + sortMethod + ' ' + filterText; 577 this.setFilterText_(filterText.trim()); 578}; 579 580EventsView.prototype.sortById_ = function(event) { 581 this.toggleSortMethod_('id'); 582}; 583 584EventsView.prototype.sortBySourceType_ = function(event) { 585 this.toggleSortMethod_('source'); 586}; 587 588EventsView.prototype.sortByDescription_ = function(event) { 589 this.toggleSortMethod_('desc'); 590}; 591 592EventsView.prototype.modifySelectionArray = function( 593 sourceEntry, addToSelection) { 594 // Find the index for |sourceEntry| in the current selection list. 595 var index = -1; 596 for (var i = 0; i < this.currentSelectedSources_.length; ++i) { 597 if (this.currentSelectedSources_[i] == sourceEntry) { 598 index = i; 599 break; 600 } 601 } 602 603 if (index != -1 && !addToSelection) { 604 // Remove from the selection. 605 this.currentSelectedSources_.splice(index, 1); 606 } 607 608 if (index == -1 && addToSelection) { 609 this.currentSelectedSources_.push(sourceEntry); 610 } 611}; 612 613EventsView.prototype.invalidateDetailsView_ = function() { 614 this.detailsView_.setData(this.currentSelectedSources_); 615}; 616 617EventsView.prototype.invalidateFilterCounter_ = function() { 618 if (!this.outstandingRepaintFilterCounter_) { 619 this.outstandingRepaintFilterCounter_ = true; 620 window.setTimeout(this.repaintFilterCounter_.bind(this), 621 EventsView.REPAINT_FILTER_COUNTER_TIMEOUT_MS); 622 } 623}; 624 625EventsView.prototype.repaintFilterCounter_ = function() { 626 this.outstandingRepaintFilterCounter_ = false; 627 this.filterCount_.innerHTML = ''; 628 addTextNode(this.filterCount_, 629 this.numPostfilter_ + ' of ' + this.numPrefilter_); 630}; 631