diff options
author | shinfan@chromium.org <shinfan@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-06-27 09:08:48 +0000 |
---|---|---|
committer | shinfan@chromium.org <shinfan@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-06-27 09:08:48 +0000 |
commit | d60c81a0455c34c6280f9603cba357e840419293 (patch) | |
tree | beb66eacf394754db8db9cca2e5a1ff78d777bac | |
parent | 393c77583781f32afbe6ef5357e756219d6771f0 (diff) | |
download | chromium_src-d60c81a0455c34c6280f9603cba357e840419293.zip chromium_src-d60c81a0455c34c6280f9603cba357e840419293.tar.gz chromium_src-d60c81a0455c34c6280f9603cba357e840419293.tar.bz2 |
Add a text marker feature to CrosLogAnalyzer
Add a text marker feature to log Analyzer in chrome://net-internals/ to
help developers view and highlight log text.
- Support multiple colors
- Auto resolve mark conflicts
- Dynamically enable/disable highlights
TEST=manual
BUG=246661
Review URL: https://chromiumcodereview.appspot.com/17176007
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@208868 0039d316-1c4b-4281-b951-d872f2087c98
5 files changed, 484 insertions, 7 deletions
diff --git a/chrome/browser/resources/net_internals/cros_log_analyzer_view.css b/chrome/browser/resources/net_internals/cros_log_analyzer_view.css index 23e4a73..098190c 100644 --- a/chrome/browser/resources/net_internals/cros_log_analyzer_view.css +++ b/chrome/browser/resources/net_internals/cros_log_analyzer_view.css @@ -112,9 +112,10 @@ border-radius: 5px; font-size: 12px; margin-bottom: 5px; + margin-left: 720px; margin-top: -40px; padding: 5px; - width: 240px; + width: 280px; } #cros-log-analyzer-search-container input { @@ -169,6 +170,54 @@ -webkit-animation: fade 1s linear 1; } +#cros-log-analyzer-marker-container { + background-color: rgb(242, 242, 242); + border: 1px solid rgb(211, 211, 211); + left: 1030px; + min-height: 320px; + padding: 10px; + position: absolute; + top: 63px; + width: 200px; +} + +.cros-log-analyzer-marker-history-entry { + display: -webkit-flex; + height: 20px; +} + + +.cros-log-analyzer-marker-history-entry * { + display: block; +} + +.cros-log-analyzer-marker-history-entry p { + font-size: 13px; + height: 15px; + margin: 0; + width: 98px; +} + +.cros-log-analyzer-marker-history-entry a { + color: grey; + font-size: 11px; +} + +.cros-log-analyzer-marker-history-color-tag { + border-radius: 10px; + height: 10px; + margin: 4px; + width: 10px; +} + +.cros-log-analyzer-marker-highlight { + font-weight: bold; +} + +#cros-log-analyzer-save-btn { + background-color: rgb(211, 211, 211); +} + @-webkit-keyframes fade { 0% { diff --git a/chrome/browser/resources/net_internals/cros_log_analyzer_view.html b/chrome/browser/resources/net_internals/cros_log_analyzer_view.html index 8c9080a..a23543c5 100644 --- a/chrome/browser/resources/net_internals/cros_log_analyzer_view.html +++ b/chrome/browser/resources/net_internals/cros_log_analyzer_view.html @@ -3,11 +3,14 @@ <div id="cros-log-analyzer-header"> Network Log </div> - <div id='cros-log-analyzer-search-container' class='cros-log-analyzer-container'> + <div id='cros-log-analyzer-search-container' + class='cros-log-analyzer-container'> Search: <input type='text' id='cros-log-analyzer-search-input'> + <span id='cros-log-analyzer-save-btn'>Save</span> </div> - <table id="cros-log-analyzer-log-header-table" class="cros-log-analyzer-container"> + <table id="cros-log-analyzer-log-header-table" + class="cros-log-analyzer-container"> <tr> <td class="cros-log-analyzer-td-level">Level</td> <td class="cros-log-analyzer-td-time">Time</td> @@ -20,7 +23,10 @@ <table id="cros-log-analyzer-log-table"> </table> </div> - <div id='cros-log-analyzer-filter-container' class='cros-log-analyzer-container'> + <div id='cros-log-analyzer-marker-container'> + </div> + <div id='cros-log-analyzer-filter-container' + class='cros-log-analyzer-container'> <div id='cros-log-analyzer-filter-pname'> </div> <div id='cros-log-analyzer-filter-level'> diff --git a/chrome/browser/resources/net_internals/cros_log_analyzer_view.js b/chrome/browser/resources/net_internals/cros_log_analyzer_view.js index 4efadce..fa6927a 100644 --- a/chrome/browser/resources/net_internals/cros_log_analyzer_view.js +++ b/chrome/browser/resources/net_internals/cros_log_analyzer_view.js @@ -67,6 +67,7 @@ var CrosLogAnalyzerView = (function() { CrosLogAnalyzerView.LOG_TABLE_ID = 'cros-log-analyzer-log-table'; CrosLogAnalyzerView.LOG_FILTER_PNAME_ID = 'cros-log-analyzer-filter-pname'; CrosLogAnalyzerView.LOG_SEARCH_INPUT_ID = 'cros-log-analyzer-search-input'; + CrosLogAnalyzerView.LOG_SEARCH_SAVE_BTN_ID = 'cros-log-analyzer-save-btn'; CrosLogAnalyzerView.LOG_VISUALIZER_CONTAINER_ID = 'cros-log-analyzer-visualizer-container'; @@ -97,7 +98,22 @@ var CrosLogAnalyzerView = (function() { g_browser.addSystemLogObserver(this); $(CrosLogAnalyzerView.LOG_SEARCH_INPUT_ID).addEventListener('keyup', this.onSearchQueryChange_.bind(this)); + $(CrosLogAnalyzerView.LOG_SEARCH_SAVE_BTN_ID).addEventListener( + 'click', this.onSaveBtnClicked_.bind(this)); + }, + /** + * Called when the save button is clicked. Saves the current filter query + * to the mark history. And highlights the matched text with colors. + */ + onSaveBtnClicked_: function() { + this.marker.addMarkHistory(this.currentQuery); + // Clears the filter query + $(CrosLogAnalyzerView.LOG_SEARCH_INPUT_ID).value = ''; + this.currentQuery = ''; + // Refresh the table + this.populateTable(); + this.filterLog(); }, onSearchQueryChange_: function() { @@ -114,8 +130,10 @@ var CrosLogAnalyzerView = (function() { populateTable: function() { var logTable = $(CrosLogAnalyzerView.LOG_TABLE_ID); logTable.innerHTML = ''; + this.tableEntries.length = 0; // Create entries for (var i = 0; i < this.logEntries.length; i++) { + this.logEntries[i].rowNum = i; var row = this.createTableRow(this.logEntries[i]); logTable.appendChild(row); } @@ -143,15 +161,15 @@ var CrosLogAnalyzerView = (function() { // Process name cell cells[2].className = LOG_CELL_PNAME_CLASSNAME; - cells[2].textContent = entry.processName; + this.marker.getHighlightedEntry(entry, 'processName', cells[2]); // Process ID cell cells[3].className = LOG_CELL_PID_CLASSNAME; - cells[3].textContent = entry.processID; + this.marker.getHighlightedEntry(entry, 'processID', cells[3]); // Description cell cells[4].className = LOG_CELL_DESCRIPTION_CLASSNAME; - cells[4].textContent = entry.description; + this.marker.getHighlightedEntry(entry, 'description', cells[4]); // Add the row into this.tableEntries for future reference this.tableEntries.push(row); @@ -163,6 +181,7 @@ var CrosLogAnalyzerView = (function() { */ refresh: function() { this.createFilter(); + this.createLogMaker(); this.populateTable(); this.createVisualizer(); }, @@ -303,6 +322,14 @@ var CrosLogAnalyzerView = (function() { }, /** + * Called during the initialization. It creates the log marker that + * highlights log text. + */ + createLogMaker: function() { + this.marker = new CrosLogMarker(this); + }, + + /** * Given a row text line of log, a logEntry instance is initialized and used * for parsing. After the text is parsed, we put the instance into * logEntries which is an array for storing. This function is called when diff --git a/chrome/browser/resources/net_internals/cros_log_marker.js b/chrome/browser/resources/net_internals/cros_log_marker.js new file mode 100644 index 0000000..71201e3 --- /dev/null +++ b/chrome/browser/resources/net_internals/cros_log_marker.js @@ -0,0 +1,394 @@ +// Copyright 2013 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. +/** + * This class stores the filter queries as history and highlight the log text + * to increase the readability of the log + * + * - Enable / Disable highlights + * - Highlights text with multiple colors + * - Resolve hghlight conficts (A text highlighted by multiple colors) so that + * the latest added highlight always has highest priority to display. + * + */ +var CrosLogMarker = (function() { + 'use strict'; + + // Special classes (defined in log_analyzer_view.css) + var LOG_MARKER_HIGHLIGHT_CLASS = 'cros-log-analyzer-marker-highlight'; + var LOG_MARKER_CONTAINER_ID = 'cros-log-analyzer-marker-container'; + var LOG_MARKER_HISTORY_ENTRY_CLASS = 'cros-log-analyzer-marker-history-entry'; + var LOG_MARKER_HISTORY_COLOR_TAG_CLASS = + 'cros-log-analyzer-marker-history-color-tag'; + + /** + * Colors used for highlighting. (Current we support 6 colors) + * TODO(shinfan): Add more supoorted colors. + */ + var COLOR_USAGE_SET = { + 'Crimson': false, + 'DeepSkyBlue': false, + 'DarkSeaGreen': false, + 'GoldenRod': false, + 'IndianRed': false, + 'Orange': false + }; + var COLOR_NUMBER = Object.keys(COLOR_USAGE_SET).length; + + + /** + * CrosHighlightTag represents a single highlight tag in text. + */ + var CrosHighlightTag = (function() { + /** + * @constructor + */ + function CrosHighlightTag(color, field, range, priority) { + this.color = color; + this.field = field; + this.range = range; + this.priority = priority; + this.enabled = true; + } + + return CrosHighlightTag; + })(); + + /** + * @constructor + * @param {CrosLogAnalyzerView} logAnalyzerView A reference to + * CrosLogAnalyzerView. + */ + function CrosLogMarker(logAnalyzerView) { + this.container = $(LOG_MARKER_CONTAINER_ID); + // Stores highlight objects for each entry. + this.entryHighlights = []; + // Stores all the filter queries. + this.markHistory = {}; + // Object references from CrosLogAnalyzerView. + this.logEntries = logAnalyzerView.logEntries; + this.logAnalyzerView = logAnalyzerView; + // Counts how many highlights are created. + this.markCount = 0; + for (var i = 0; i < this.logEntries.length; i++) { + this.entryHighlights.push([]); + } + } + + CrosLogMarker.prototype = { + /** + * Saves the query to the mark history and highlights the text + * based on the query. + */ + addMarkHistory: function(query) { + // Increases the counter + this.markCount += 1; + + // Find an avaiable color. + var color = this.pickColor(); + if (!color) { + // If all colors are occupied. + alert('You can only add at most ' + COLOR_NUMBER + 'markers.'); + return; + } + + // Updates HTML elements. + var historyEntry = addNode(this.container, 'div'); + historyEntry.className = LOG_MARKER_HISTORY_ENTRY_CLASS; + + // A color tag that indicats the color used. + var colorTag = addNode(historyEntry, 'div'); + colorTag.className = LOG_MARKER_HISTORY_COLOR_TAG_CLASS; + colorTag.style.background = color; + + // Displays the query text. + var queryText = addNodeWithText(historyEntry, 'p', query); + queryText.style.color = color; + + // Adds a button to remove the marker. + var removeBtn = addNodeWithText(historyEntry, 'a', 'Remove'); + removeBtn.addEventListener( + 'click', this.onRemoveBtnClicked_.bind(this, historyEntry, color)); + + // A checkbox that lets user enable and disable the marker. + var enableCheckbox = addNode(historyEntry, 'input'); + enableCheckbox.type = 'checkbox'; + enableCheckbox.checked = true; + enableCheckbox.color = color; + enableCheckbox.addEventListener('change', + this.onEnableCheckboxChange_.bind(this, enableCheckbox), false); + + // Searches log text for matched patterns and highlights them. + this.patternMatch(query, color); + }, + + /** + * Search the text for matched strings + */ + patternMatch: function(query, color) { + var pattern = new RegExp(query, 'i'); + for (var i = 0; i < this.logEntries.length; i++) { + var entry = this.logEntries[i]; + // Search description of each log entry + // TODO(shinfan): Add more search fields + var positions = this.findPositions( + pattern, entry.description); + for (var j = 0; j < positions.length; j++) { + var pos = positions[j]; + this.mark(entry, pos, 'description', color); + } + this.sortHighlightsByStartPosition_(this.entryHighlights[i]); + } + }, + + /** + * Highlights the text. + * @param {CrosLogEntry} entry The log entry to be highlighted + * @param {int|Array} position [start, end] + * @param {string} field The field of entry to be highlighted + * @param {string} color color used for highlighting + */ + mark: function(entry, position, field, color) { + // Creates the highlight object + var tag = new CrosHighlightTag(color, field, position, this.markCount); + // Add the highlight into entryHighlights + this.entryHighlights[entry.rowNum].push(tag); + }, + + /** + * Find the highlight objects that covers the given position + * @param {CrosHighlightTag|Array} highlights highlights of a log entry + * @param {int} position The target index + * @param {string} field The target field + * @return {CrosHighlightTag|Array} Highlights that cover the position + */ + getHighlight: function(highlights, index, field) { + var res = []; + for (var j = 0; j < highlights.length; j++) { + var highlight = highlights[j]; + if (highlight.range[0] <= index && + highlight.range[1] > index && + highlight.field == field && + highlight.enabled) { + res.push(highlight); + } + } + /** + * Sorts the result by priority so that the highlight with + * highest priority comes first. + */ + this.sortHighlightsByPriority_(res); + return res; + }, + + /** + * This function highlights the entry by going through the text from left + * to right and searching for "key" positions. + * A "key" position is a position that one (or more) highlight + * starts or ends. We only care about "key" positions because this is where + * the text highlight status changes. + * At each key position, the function decides if the text between this + * position and previous position need to be highlighted and resolves + * highlight conflicts. + * + * @param {CrosLogEntry} entry The entry going to be highlighted. + * @param {string} field The specified field of the entry. + * @param {DOMElement} parent Parent node. + */ + getHighlightedEntry: function(entry, field, parent) { + var rowNum = entry.rowNum; + // Get the original text content of the entry (without any highlights). + var content = this.logEntries[rowNum][field]; + var index = 0; + while (index < content.length) { + var nextIndex = this.getNextIndex( + this.entryHighlights[rowNum], index, field, content); + // Searches for highlights that have the position in range. + var highlights = this.getHighlight( + this.entryHighlights[rowNum], index, field); + var text = content.substr(index, nextIndex - index); + if (highlights.length > 0) { + // Always picks the highlight with highest priority. + this.addSpan(text, highlights[0].color, parent); + } else { + addNodeWithText(parent, 'span', text); + } + index = nextIndex; + } + }, + + /** + * A helper function that is used by this.getHightlightedEntry + * It returns the first index where a highlight begins or ends from + * the given index. + * @param {CrosHighlightTag|Array} highlights An array of highlights + * of a log entry. + * @param {int} index The start position. + * @param {string} field The specified field of entry. + * Other fields are ignored. + * @param {string} content The text content of the log entry. + * @return {int} The first index where a highlight begins or ends. + */ + getNextIndex: function(highlights, index, field, content) { + var minGap = Infinity; + var res = -1; + for (var i = 0; i < highlights.length; i++) { + if (highlights[i].field != field || !highlights[i].enabled) + continue; + // Distance between current index and the start index of highlight. + var gap1 = highlights[i].range[0] - index; + // Distance between current index and the end index of highlight. + var gap2 = highlights[i].range[1] - index; + if (gap1 > 0 && gap1 < minGap) { + minGap = gap1; + res = highlights[i].range[0]; + } + if (gap2 > 0 && gap2 < minGap) { + minGap = gap2; + res = highlights[i].range[1]; + } + } + // Returns |res| if found. Otherwise returns the end position of the text. + return res > 0 ? res : content.length; + }, + + /** + * A helper function that is used by this.getHightlightedEntry. + * It adds the HTML label to the text. + */ + addSpan: function(text, color, parent) { + var span = addNodeWithText(parent, 'span', text); + span.style.color = color; + span.className = LOG_MARKER_HIGHLIGHT_CLASS; + }, + + /** + * A helper function that is used by this.getHightlightedEntry. + * It adds the HTML label to the text. + */ + pickColor: function() { + for (var color in COLOR_USAGE_SET) { + if (!COLOR_USAGE_SET[color]) { + COLOR_USAGE_SET[color] = true; + return color; + } + } + return false; + }, + + /** + * A event handler that enables and disables the corresponding marker. + * @private + */ + onEnableCheckboxChange_: function(checkbox) { + for (var i = 0; i < this.entryHighlights.length; i++) { + for (var j = 0; j < this.entryHighlights[i].length; j++) { + if (this.entryHighlights[i][j].color == checkbox.color) { + this.entryHighlights[i][j].enabled = checkbox.checked; + } + } + } + this.refreshLogTable(); + }, + + /** + * A event handlier that removes the marker from history. + * @private + */ + onRemoveBtnClicked_: function(entry, color) { + entry.parentNode.removeChild(entry); + COLOR_USAGE_SET[color] = false; + for (var i = 0; i < this.entryHighlights.length; i++) { + var highlights = this.entryHighlights[i]; + while (true) { + var index = this.findHighlightByColor_(highlights, color); + if (index == -1) + break; + highlights.splice(index, 1); + } + } + this.refreshLogTable(); + }, + + /** + * A helper function that returns the index of first highlight that + * has the target color. Otherwise returns -1. + * @private + */ + findHighlightByColor_: function(highlights, color) { + for (var i = 0; i < highlights.length; i++) { + if (highlights[i].color == color) + return i; + } + return -1; + }, + + /** + * Refresh the log table in the CrosLogAnalyzerView. + */ + refreshLogTable: function() { + this.logAnalyzerView.populateTable(); + this.logAnalyzerView.filterLog(); + }, + + /** + * A pattern can appear multiple times in a string. + * Returns positions of all the appearance. + */ + findPositions: function(pattern, str) { + var res = []; + str = str.toLowerCase(); + var match = str.match(pattern); + if (!match) + return res; + for (var i = 0; i < match.length; i++) { + var index = 0; + while (true) { + var start = str.indexOf(match[i].toLowerCase(), index); + if (start == -1) + break; + var end = start + match[i].length; + res.push([start, end]); + index = end + 1; + } + } + return res; + }, + + /** + * A helper function used in sorting highlights by start position. + * @param {HighlightTag} h1, h2 Two highlight tags in the array. + * @private + */ + compareStartPosition_: function(h1, h2) { + return h1.range[0] - h2.range[0]; + }, + + /** + * A helper function used in sorting highlights by priority. + * @param {HighlightTag} h1, h2 Two highlight tags in the array. + * @private + */ + comparePriority_: function(h1, h2) { + return h2.priority - h1.priority; + }, + + /** + * A helper function that sorts the highlights array by start position. + * @private + */ + sortHighlightsByStartPosition_: function(highlights) { + highlights.sort(this.compareStartPosition_); + }, + + /** + * A helper function that sorts the highlights array by priority. + * @private + */ + sortHighlightsByPriority_: function(highlights) { + highlights.sort(this.comparePriority_); + } + }; + + return CrosLogMarker; +})(); diff --git a/chrome/browser/resources/net_internals/index.js b/chrome/browser/resources/net_internals/index.js index 762b036..44465fd 100644 --- a/chrome/browser/resources/net_internals/index.js +++ b/chrome/browser/resources/net_internals/index.js @@ -51,6 +51,7 @@ <include src="cros_log_analyzer_view.js"/> <include src="cros_log_entry.js"/> <include src="cros_log_visualizer.js" /> +<include src="cros_log_marker.js" /> document.addEventListener('DOMContentLoaded', function() { MainView.getInstance(); // from main.js |