1 // Copyright (c) 2012 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 * | || | 24 * +----------------------++----------------+ 25 */ 26 var EventsView = (function() { 27 'use strict'; 28 29 // How soon after updating the filter list the counter should be updated. 30 var REPAINT_FILTER_COUNTER_TIMEOUT_MS = 0; 31 32 // We inherit from View. 33 var superClass = View; 34 35 /* 36 * @constructor 37 */ 38 function EventsView() { 39 assertFirstConstructorCall(EventsView); 40 41 // Call superclass's constructor. 42 superClass.call(this); 43 44 // Initialize the sub-views. 45 var leftPane = new VerticalSplitView(new DivView(EventsView.TOPBAR_ID), 46 new DivView(EventsView.LIST_BOX_ID)); 47 48 this.detailsView_ = new DetailsView(EventsView.DETAILS_LOG_BOX_ID); 49 50 this.splitterView_ = new ResizableVerticalSplitView( 51 leftPane, this.detailsView_, new DivView(EventsView.SIZER_ID)); 52 53 SourceTracker.getInstance().addSourceEntryObserver(this); 54 55 this.tableBody_ = $(EventsView.TBODY_ID); 56 57 this.filterInput_ = $(EventsView.FILTER_INPUT_ID); 58 this.filterCount_ = $(EventsView.FILTER_COUNT_ID); 59 60 this.filterInput_.addEventListener('search', 61 this.onFilterTextChanged_.bind(this), true); 62 63 $(EventsView.SELECT_ALL_ID).addEventListener( 64 'click', this.selectAll_.bind(this), true); 65 66 $(EventsView.SORT_BY_ID_ID).addEventListener( 67 'click', this.sortById_.bind(this), true); 68 69 $(EventsView.SORT_BY_SOURCE_TYPE_ID).addEventListener( 70 'click', this.sortBySourceType_.bind(this), true); 71 72 $(EventsView.SORT_BY_DESCRIPTION_ID).addEventListener( 73 'click', this.sortByDescription_.bind(this), true); 74 75 new MouseOverHelp(EventsView.FILTER_HELP_ID, 76 EventsView.FILTER_HELP_HOVER_ID); 77 78 // Sets sort order and filter. 79 this.setFilter_(''); 80 81 this.initializeSourceList_(); 82 } 83 84 EventsView.TAB_ID = 'tab-handle-events'; 85 EventsView.TAB_NAME = 'Events'; 86 EventsView.TAB_HASH = '#events'; 87 88 // IDs for special HTML elements in events_view.html 89 EventsView.TBODY_ID = 'events-view-source-list-tbody'; 90 EventsView.FILTER_INPUT_ID = 'events-view-filter-input'; 91 EventsView.FILTER_COUNT_ID = 'events-view-filter-count'; 92 EventsView.FILTER_HELP_ID = 'events-view-filter-help'; 93 EventsView.FILTER_HELP_HOVER_ID = 'events-view-filter-help-hover'; 94 EventsView.SELECT_ALL_ID = 'events-view-select-all'; 95 EventsView.SORT_BY_ID_ID = 'events-view-sort-by-id'; 96 EventsView.SORT_BY_SOURCE_TYPE_ID = 'events-view-sort-by-source'; 97 EventsView.SORT_BY_DESCRIPTION_ID = 'events-view-sort-by-description'; 98 EventsView.DETAILS_LOG_BOX_ID = 'events-view-details-log-box'; 99 EventsView.TOPBAR_ID = 'events-view-filter-box'; 100 EventsView.LIST_BOX_ID = 'events-view-source-list'; 101 EventsView.SIZER_ID = 'events-view-splitter-box'; 102 103 cr.addSingletonGetter(EventsView); 104 105 EventsView.prototype = { 106 // Inherit the superclass's methods. 107 __proto__: superClass.prototype, 108 109 /** 110 * Initializes the list of source entries. If source entries are already, 111 * being displayed, removes them all in the process. 112 */ 113 initializeSourceList_: function() { 114 this.currentSelectedRows_ = []; 115 this.sourceIdToRowMap_ = {}; 116 this.tableBody_.innerHTML = ''; 117 this.numPrefilter_ = 0; 118 this.numPostfilter_ = 0; 119 this.invalidateFilterCounter_(); 120 this.invalidateDetailsView_(); 121 }, 122 123 setGeometry: function(left, top, width, height) { 124 superClass.prototype.setGeometry.call(this, left, top, width, height); 125 this.splitterView_.setGeometry(left, top, width, height); 126 }, 127 128 show: function(isVisible) { 129 superClass.prototype.show.call(this, isVisible); 130 this.splitterView_.show(isVisible); 131 }, 132 133 getFilterText_: function() { 134 return this.filterInput_.value; 135 }, 136 137 setFilterText_: function(filterText) { 138 this.filterInput_.value = filterText; 139 this.onFilterTextChanged_(); 140 }, 141 142 onFilterTextChanged_: function() { 143 this.setFilter_(this.getFilterText_()); 144 }, 145 146 /** 147 * Updates text in the details view when privacy stripping is toggled. 148 */ 149 onPrivacyStrippingChanged: function() { 150 this.invalidateDetailsView_(); 151 }, 152 153 comparisonFuncWithReversing_: function(a, b) { 154 var result = this.comparisonFunction_(a, b); 155 if (this.doSortBackwards_) 156 result *= -1; 157 return result; 158 }, 159 160 sort_: function() { 161 var sourceEntries = []; 162 for (var id in this.sourceIdToRowMap_) { 163 sourceEntries.push(this.sourceIdToRowMap_[id].getSourceEntry()); 164 } 165 sourceEntries.sort(this.comparisonFuncWithReversing_.bind(this)); 166 167 // Reposition source rows from back to front. 168 for (var i = sourceEntries.length - 2; i >= 0; --i) { 169 var sourceRow = this.sourceIdToRowMap_[sourceEntries[i].getSourceId()]; 170 var nextSourceId = sourceEntries[i + 1].getSourceId(); 171 if (sourceRow.getNextNodeSourceId() != nextSourceId) { 172 var nextSourceRow = this.sourceIdToRowMap_[nextSourceId]; 173 sourceRow.moveBefore(nextSourceRow); 174 } 175 } 176 }, 177 178 setFilter_: function(filterText) { 179 var lastComparisonFunction = this.comparisonFunction_; 180 var lastDoSortBackwards = this.doSortBackwards_; 181 182 var filterParser = new SourceFilterParser(filterText); 183 this.currentFilter_ = filterParser.filter; 184 185 this.pickSortFunction_(filterParser.sort); 186 187 if (lastComparisonFunction != this.comparisonFunction_ || 188 lastDoSortBackwards != this.doSortBackwards_) { 189 this.sort_(); 190 } 191 192 // Iterate through all of the rows and see if they match the filter. 193 for (var id in this.sourceIdToRowMap_) { 194 var entry = this.sourceIdToRowMap_[id]; 195 entry.setIsMatchedByFilter(this.currentFilter_(entry.getSourceEntry())); 196 } 197 }, 198 199 /** 200 * Given a "sort" object with "method" and "backwards" keys, looks up and 201 * sets |comparisonFunction_| and |doSortBackwards_|. If the ID does not 202 * correspond to a sort function, defaults to sorting by ID. 203 */ 204 pickSortFunction_: function(sort) { 205 this.doSortBackwards_ = sort.backwards; 206 this.comparisonFunction_ = COMPARISON_FUNCTION_TABLE[sort.method]; 207 if (!this.comparisonFunction_) { 208 this.doSortBackwards_ = false; 209 this.comparisonFunction_ = compareSourceId_; 210 } 211 }, 212 213 /** 214 * Repositions |sourceRow|'s in the table using an insertion sort. 215 * Significantly faster than sorting the entire table again, when only 216 * one entry has changed. 217 */ 218 insertionSort_: function(sourceRow) { 219 // SourceRow that should be after |sourceRow|, if it needs 220 // to be moved earlier in the list. 221 var sourceRowAfter = sourceRow; 222 while (true) { 223 var prevSourceId = sourceRowAfter.getPreviousNodeSourceId(); 224 if (prevSourceId == null) 225 break; 226 var prevSourceRow = this.sourceIdToRowMap_[prevSourceId]; 227 if (this.comparisonFuncWithReversing_( 228 sourceRow.getSourceEntry(), 229 prevSourceRow.getSourceEntry()) >= 0) { 230 break; 231 } 232 sourceRowAfter = prevSourceRow; 233 } 234 if (sourceRowAfter != sourceRow) { 235 sourceRow.moveBefore(sourceRowAfter); 236 return; 237 } 238 239 var sourceRowBefore = sourceRow; 240 while (true) { 241 var nextSourceId = sourceRowBefore.getNextNodeSourceId(); 242 if (nextSourceId == null) 243 break; 244 var nextSourceRow = this.sourceIdToRowMap_[nextSourceId]; 245 if (this.comparisonFuncWithReversing_( 246 sourceRow.getSourceEntry(), 247 nextSourceRow.getSourceEntry()) <= 0) { 248 break; 249 } 250 sourceRowBefore = nextSourceRow; 251 } 252 if (sourceRowBefore != sourceRow) 253 sourceRow.moveAfter(sourceRowBefore); 254 }, 255 256 /** 257 * Called whenever SourceEntries are updated with new log entries. Updates 258 * the corresponding table rows, sort order, and the details view as needed. 259 */ 260 onSourceEntriesUpdated: function(sourceEntries) { 261 var isUpdatedSourceSelected = false; 262 var numNewSourceEntries = 0; 263 264 for (var i = 0; i < sourceEntries.length; ++i) { 265 var sourceEntry = sourceEntries[i]; 266 267 // Lookup the row. 268 var sourceRow = this.sourceIdToRowMap_[sourceEntry.getSourceId()]; 269 270 if (!sourceRow) { 271 sourceRow = new SourceRow(this, sourceEntry); 272 this.sourceIdToRowMap_[sourceEntry.getSourceId()] = sourceRow; 273 ++numNewSourceEntries; 274 } else { 275 sourceRow.onSourceUpdated(); 276 } 277 278 if (sourceRow.isSelected()) 279 isUpdatedSourceSelected = true; 280 281 // TODO(mmenke): Fix sorting when sorting by duration. 282 // Duration continuously increases for all entries that 283 // are still active. This can result in incorrect 284 // sorting, until sort_ is called. 285 this.insertionSort_(sourceRow); 286 } 287 288 if (isUpdatedSourceSelected) 289 this.invalidateDetailsView_(); 290 if (numNewSourceEntries) 291 this.incrementPrefilterCount(numNewSourceEntries); 292 }, 293 294 /** 295 * Returns the SourceRow with the specified ID, if there is one. 296 * Otherwise, returns undefined. 297 */ 298 getSourceRow: function(id) { 299 return this.sourceIdToRowMap_[id]; 300 }, 301 302 /** 303 * Called whenever all log events are deleted. 304 */ 305 onAllSourceEntriesDeleted: function() { 306 this.initializeSourceList_(); 307 }, 308 309 /** 310 * Called when either a log file is loaded, after clearing the old entries, 311 * but before getting any new ones. 312 */ 313 onLoadLogStart: function() { 314 // Needed to sort new sourceless entries correctly. 315 this.maxReceivedSourceId_ = 0; 316 }, 317 318 onLoadLogFinish: function(data) { 319 return true; 320 }, 321 322 incrementPrefilterCount: function(offset) { 323 this.numPrefilter_ += offset; 324 this.invalidateFilterCounter_(); 325 }, 326 327 incrementPostfilterCount: function(offset) { 328 this.numPostfilter_ += offset; 329 this.invalidateFilterCounter_(); 330 }, 331 332 onSelectionChanged: function() { 333 this.invalidateDetailsView_(); 334 }, 335 336 clearSelection: function() { 337 var prevSelection = this.currentSelectedRows_; 338 this.currentSelectedRows_ = []; 339 340 // Unselect everything that is currently selected. 341 for (var i = 0; i < prevSelection.length; ++i) { 342 prevSelection[i].setSelected(false); 343 } 344 345 this.onSelectionChanged(); 346 }, 347 348 selectAll_: function(event) { 349 for (var id in this.sourceIdToRowMap_) { 350 var sourceRow = this.sourceIdToRowMap_[id]; 351 if (sourceRow.isMatchedByFilter()) { 352 sourceRow.setSelected(true); 353 } 354 } 355 event.preventDefault(); 356 }, 357 358 unselectAll_: function() { 359 var entries = this.currentSelectedRows_.slice(0); 360 for (var i = 0; i < entries.length; ++i) { 361 entries[i].setSelected(false); 362 } 363 }, 364 365 /** 366 * If |params| includes a query, replaces the current filter and unselects. 367 * all items. If it includes a selection, tries to select the relevant 368 * item. 369 */ 370 setParameters: function(params) { 371 if (params.q) { 372 this.unselectAll_(); 373 this.setFilterText_(params.q); 374 } 375 376 if (params.s) { 377 var sourceRow = this.sourceIdToRowMap_[params.s]; 378 if (sourceRow) { 379 sourceRow.setSelected(true); 380 this.scrollToSourceId(params.s); 381 } 382 } 383 }, 384 385 /** 386 * Scrolls to the source indicated by |sourceId|, if displayed. 387 */ 388 scrollToSourceId: function(sourceId) { 389 this.detailsView_.scrollToSourceId(sourceId); 390 }, 391 392 /** 393 * If already using the specified sort method, flips direction. Otherwise, 394 * removes pre-existing sort parameter before adding the new one. 395 */ 396 toggleSortMethod_: function(sortMethod) { 397 // Get old filter text and remove old sort directives, if any. 398 var filterParser = new SourceFilterParser(this.getFilterText_()); 399 var filterText = filterParser.filterTextWithoutSort; 400 401 filterText = 'sort:' + sortMethod + ' ' + filterText; 402 403 // If already using specified sortMethod, sort backwards. 404 if (!this.doSortBackwards_ && 405 COMPARISON_FUNCTION_TABLE[sortMethod] == this.comparisonFunction_) { 406 filterText = '-' + filterText; 407 } 408 409 this.setFilterText_(filterText.trim()); 410 }, 411 412 sortById_: function(event) { 413 this.toggleSortMethod_('id'); 414 }, 415 416 sortBySourceType_: function(event) { 417 this.toggleSortMethod_('source'); 418 }, 419 420 sortByDescription_: function(event) { 421 this.toggleSortMethod_('desc'); 422 }, 423 424 /** 425 * Modifies the map of selected rows to include/exclude the one with 426 * |sourceId|, if present. Does not modify checkboxes or the LogView. 427 * Should only be called by a SourceRow in response to its selection 428 * state changing. 429 */ 430 modifySelectionArray: function(sourceId, addToSelection) { 431 var sourceRow = this.sourceIdToRowMap_[sourceId]; 432 if (!sourceRow) 433 return; 434 // Find the index for |sourceEntry| in the current selection list. 435 var index = -1; 436 for (var i = 0; i < this.currentSelectedRows_.length; ++i) { 437 if (this.currentSelectedRows_[i] == sourceRow) { 438 index = i; 439 break; 440 } 441 } 442 443 if (index != -1 && !addToSelection) { 444 // Remove from the selection. 445 this.currentSelectedRows_.splice(index, 1); 446 } 447 448 if (index == -1 && addToSelection) { 449 this.currentSelectedRows_.push(sourceRow); 450 } 451 }, 452 453 getSelectedSourceEntries_: function() { 454 var sourceEntries = []; 455 for (var i = 0; i < this.currentSelectedRows_.length; ++i) { 456 sourceEntries.push(this.currentSelectedRows_[i].getSourceEntry()); 457 } 458 return sourceEntries; 459 }, 460 461 invalidateDetailsView_: function() { 462 this.detailsView_.setData(this.getSelectedSourceEntries_()); 463 }, 464 465 invalidateFilterCounter_: function() { 466 if (!this.outstandingRepaintFilterCounter_) { 467 this.outstandingRepaintFilterCounter_ = true; 468 window.setTimeout(this.repaintFilterCounter_.bind(this), 469 REPAINT_FILTER_COUNTER_TIMEOUT_MS); 470 } 471 }, 472 473 repaintFilterCounter_: function() { 474 this.outstandingRepaintFilterCounter_ = false; 475 this.filterCount_.innerHTML = ''; 476 addTextNode(this.filterCount_, 477 this.numPostfilter_ + ' of ' + this.numPrefilter_); 478 } 479 }; // end of prototype. 480 481 // ------------------------------------------------------------------------ 482 // Helper code for comparisons 483 // ------------------------------------------------------------------------ 484 485 var COMPARISON_FUNCTION_TABLE = { 486 // sort: and sort:- are allowed 487 '': compareSourceId_, 488 'active': compareActive_, 489 'desc': compareDescription_, 490 'description': compareDescription_, 491 'duration': compareDuration_, 492 'id': compareSourceId_, 493 'source': compareSourceType_, 494 'type': compareSourceType_ 495 }; 496 497 /** 498 * Sorts active entries first. If both entries are inactive, puts the one 499 * that was active most recently first. If both are active, uses source ID, 500 * which puts longer lived events at the top, and behaves better than using 501 * duration or time of first event. 502 */ 503 function compareActive_(source1, source2) { 504 if (!source1.isInactive() && source2.isInactive()) 505 return -1; 506 if (source1.isInactive() && !source2.isInactive()) 507 return 1; 508 if (source1.isInactive()) { 509 var deltaEndTime = source1.getEndTime() - source2.getEndTime(); 510 if (deltaEndTime != 0) { 511 // The one that ended most recently (Highest end time) should be sorted 512 // first. 513 return -deltaEndTime; 514 } 515 // If both ended at the same time, then odds are they were related events, 516 // started one after another, so sort in the opposite order of their 517 // source IDs to get a more intuitive ordering. 518 return -compareSourceId_(source1, source2); 519 } 520 return compareSourceId_(source1, source2); 521 } 522 523 function compareDescription_(source1, source2) { 524 var source1Text = source1.getDescription().toLowerCase(); 525 var source2Text = source2.getDescription().toLowerCase(); 526 var compareResult = source1Text.localeCompare(source2Text); 527 if (compareResult != 0) 528 return compareResult; 529 return compareSourceId_(source1, source2); 530 } 531 532 function compareDuration_(source1, source2) { 533 var durationDifference = source2.getDuration() - source1.getDuration(); 534 if (durationDifference) 535 return durationDifference; 536 return compareSourceId_(source1, source2); 537 } 538 539 /** 540 * For the purposes of sorting by source IDs, entries without a source 541 * appear right after the SourceEntry with the highest source ID received 542 * before the sourceless entry. Any ambiguities are resolved by ordering 543 * the entries without a source by the order in which they were received. 544 */ 545 function compareSourceId_(source1, source2) { 546 var sourceId1 = source1.getSourceId(); 547 if (sourceId1 < 0) 548 sourceId1 = source1.getMaxPreviousEntrySourceId(); 549 var sourceId2 = source2.getSourceId(); 550 if (sourceId2 < 0) 551 sourceId2 = source2.getMaxPreviousEntrySourceId(); 552 553 if (sourceId1 != sourceId2) 554 return sourceId1 - sourceId2; 555 556 // One or both have a negative ID. In either case, the source with the 557 // highest ID should be sorted first. 558 return source2.getSourceId() - source1.getSourceId(); 559 } 560 561 function compareSourceType_(source1, source2) { 562 var source1Text = source1.getSourceTypeString(); 563 var source2Text = source2.getSourceTypeString(); 564 var compareResult = source1Text.localeCompare(source2Text); 565 if (compareResult != 0) 566 return compareResult; 567 return compareSourceId_(source1, source2); 568 } 569 570 return EventsView; 571 })(); 572