Commit e1704542 authored by mmenke@chromium.org's avatar mmenke@chromium.org

about:net-internals: Add support for negative text filters,

and update the mouseover help.

BUG=116601

Review URL: https://codereview.chromium.org/13725016

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@194731 0039d316-1c4b-4281-b951-d872f2087c98
parent 6604b159
......@@ -20,7 +20,7 @@
<li>sort:desc
<li>sort:duration
<li>sort:type
<li>sort:-&lt;option&gt; Reverses sort order
<li>-sort:&lt;option&gt; Reverses sort order
</ul>
Additional filters:
......@@ -29,7 +29,7 @@
<li>type:&lt;type&gt;[,&lt;type&gt;]*
<li>is:active
<li>is:error
<li>is:-&lt;option&gt; Inverts "is" filter.
<li>-&lt;filter&gt; Inverts a filter.
</ul>
</div>
<!-- Events Box: This the panel on the left which lists the sources -->
......
......@@ -178,45 +178,34 @@ var EventsView = (function() {
var lastComparisonFunction = this.comparisonFunction_;
var lastDoSortBackwards = this.doSortBackwards_;
this.pickSortFunction_(filterText);
var filterParser = new SourceFilterParser(filterText);
this.currentFilter_ = filterParser.filter;
this.pickSortFunction_(filterParser.sort);
if (lastComparisonFunction != this.comparisonFunction_ ||
lastDoSortBackwards != this.doSortBackwards_) {
this.sort_();
}
var oldFilter = this.currentFilter_;
this.currentFilter_ = createFilter_(filterText);
// No need to filter again if filters match.
if (oldFilter &&
JSON.stringify(oldFilter) == JSON.stringify(this.currentFilter_)) {
return;
}
// Iterate through all of the rows and see if they match the filter.
for (var id in this.sourceIdToRowMap_) {
var entry = this.sourceIdToRowMap_[id];
entry.setIsMatchedByFilter(entry.matchesFilter(this.currentFilter_));
entry.setIsMatchedByFilter(this.currentFilter_(entry.getSourceEntry()));
}
},
/**
* Parse any "sort:" directives, and update |comparisonFunction_| and
* |doSortBackwards_| as needed. Note only the last valid sort directive
* is used.
* Given a "sort" object with "method" and "backwards" keys, looks up and
* sets |comparisonFunction_| and |doSortBackwards_|. If the ID does not
* correspond to a sort function, defaults to sorting by ID.
*/
pickSortFunction_: function(filterText) {
this.comparisonFunction_ = compareSourceId_;
this.doSortBackwards_ = false;
var filterList = parseFilter_(filterText);
for (var i = 0; i < filterList.length; ++i) {
var sort = parseSortDirective_(filterList[i].parsed);
if (sort != null) {
this.comparisonFunction_ = sort.comparisonFunction;
this.doSortBackwards_ = sort.backwards;
}
pickSortFunction_: function(sort) {
this.doSortBackwards_ = sort.backwards;
this.comparisonFunction_ = COMPARISON_FUNCTION_TABLE[sort.method];
if (!this.comparisonFunction_) {
this.doSortBackwards_ = false;
this.comparisonFunction_ = compareSourceId_;
}
},
......@@ -405,20 +394,17 @@ var EventsView = (function() {
*/
toggleSortMethod_: function(sortMethod) {
// Get old filter text and remove old sort directives, if any.
var filterList = parseFilter_(this.getFilterText_());
var filterText = '';
for (var i = 0; i < filterList.length; ++i) {
if (parseSortDirective_(filterList[i].parsed) == null)
filterText += filterList[i].original;
}
var filterParser = new SourceFilterParser(this.getFilterText_());
var filterText = filterParser.filterTextWithoutSort;
filterText = 'sort:' + sortMethod + ' ' + filterText;
// If already using specified sortMethod, sort backwards.
if (!this.doSortBackwards_ &&
COMPARISON_FUNCTION_TABLE[sortMethod] == this.comparisonFunction_) {
sortMethod = '-' + sortMethod;
filterText = '-' + filterText;
}
filterText = 'sort:' + sortMethod + ' ' + filterText;
this.setFilterText_(filterText.trim());
},
......@@ -580,160 +566,5 @@ var EventsView = (function() {
return compareSourceId_(source1, source2);
}
/**
* Parses a single "sort:" directive, and returns a dictionary containing
* the sort function and direction. Returns null on failure, including
* the case when no such sort function exists.
*/
function parseSortDirective_(filterElement) {
var match = /^sort:(-?)(.*)$/.exec(filterElement);
if (!match || !COMPARISON_FUNCTION_TABLE[match[2]])
return null;
return {
comparisonFunction: COMPARISON_FUNCTION_TABLE[match[2]],
backwards: (match[1] == '-'),
};
}
/**
* Parses an "is:" directive, and updates |filter| accordingly.
*
* Returns true on success, and false if |filterElement| is not an "is:"
* directive.
*/
function parseRestrictDirective_(filterElement, filter) {
var match = /^is:(-?)(.*)$/.exec(filterElement);
if (!match)
return false;
if (match[2] == 'active') {
if (match[1] == '-') {
filter.isInactive = true;
} else {
filter.isActive = true;
}
return true;
}
if (match[2] == 'error') {
if (match[1] == '-') {
filter.isNotError = true;
} else {
filter.isError = true;
}
return true;
}
return false;
}
/**
* Parses all directives that take arbitrary strings as input,
* and updates |filter| accordingly. Directives of these types
* are stored as lists.
*
* Returns true on success, and false if |filterElement| is not a
* recognized directive.
*/
function parseStringDirective_(filterElement, filter) {
var directives = ['type', 'id'];
for (var i = 0; i < directives.length; ++i) {
var directive = directives[i];
var match = RegExp('^' + directive + ':(.*)$').exec(filterElement);
if (!match)
continue;
// Split parameters around commas and remove empty elements.
var parameters = match[1].split(',');
parameters = parameters.filter(function(string) {
return string.length > 0;
});
// If there's already a matching filter, take the intersection.
// This behavior primarily exists for tests. It is not correct
// when one of the 'type' filters is a partial match.
if (filter[directive]) {
parameters = parameters.filter(function(string) {
return filter[directive].indexOf(string) != -1;
});
}
filter[directive] = parameters;
return true;
}
return false;
}
/**
* Takes in the text of a filter and returns a list of {parsed, original}
* pairs that correspond to substrings of the filter before and after
* filtering. This function is used both to parse filters and to remove
* the sort rule from a filter. Extra whitespace other than a single
* character after each element is ignored. Parsed strings are all
* lowercase.
*/
function parseFilter_(filterText) {
filterText = filterText.toLowerCase();
// Assemble a list of quoted and unquoted strings in the filter.
var filterList = [];
var position = 0;
while (position < filterText.length) {
var inQuote = false;
var filterElement = '';
var startPosition = position;
while (position < filterText.length) {
var nextCharacter = filterText[position];
++position;
if (nextCharacter == '\\' &&
position < filterText.length) {
// If there's a backslash, skip the backslash and add the next
// character to the element.
filterElement += filterText[position];
++position;
continue;
} else if (nextCharacter == '"') {
// If there's an unescaped quote character, toggle |inQuote| without
// modifying the element.
inQuote = !inQuote;
} else if (!inQuote && /\s/.test(nextCharacter)) {
// If not in a quote and have a whitespace character, that's the
// end of the element.
break;
} else {
// Otherwise, add the next character to the element.
filterElement += nextCharacter;
}
}
if (filterElement.length > 0) {
var filter = {
parsed: filterElement,
original: filterText.substring(startPosition, position),
};
filterList.push(filter);
}
}
return filterList;
}
/**
* Converts |filterText| into an object representing the filter.
*/
function createFilter_(filterText) {
var filter = {};
var filterList = parseFilter_(filterText);
for (var i = 0; i < filterList.length; ++i) {
if (parseSortDirective_(filterList[i].parsed) ||
parseRestrictDirective_(filterList[i].parsed, filter) ||
parseStringDirective_(filterList[i].parsed, filter)) {
continue;
}
if (filter.textFilters == undefined)
filter.textFilters = [];
filter.textFilters.push(filterList[i].parsed);
}
return filter;
}
return EventsView;
})();
......@@ -25,6 +25,7 @@
<include src="halted_status_view.js"/>
<include src="status_view.js"/>
<include src="dns_view.js"/>
<include src="source_filter_parser.js"/>
<include src="source_row.js"/>
<include src="events_view.js"/>
<include src="details_view.js"/>
......
......@@ -114,5 +114,7 @@ table.styled-table,
* Styling for elements that show a help window on mouse over.
*/
.mouse-over-help-hover {
color: blue;
cursor: help;
font-weight: bold;
}
// Copyright (c) 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.
var SourceFilterParser = (function() {
'use strict';
/**
* Parses |filterText|, extracting a sort method, a list of filters, and a
* copy of |filterText| with all sort parameters removed.
*/
function SourceFilterParser(filterText) {
// Final output will be stored here.
this.filter = null;
this.sort = {};
this.filterTextWithoutSort = '';
var filterList = parseFilter_(filterText);
// Text filters are stored here as strings and then added as a function at
// the end, for performance reasons.
var textFilters = [];
// Filter functions are first created individually, and then merged.
var filterFunctions = [];
for (var i = 0; i < filterList.length; ++i) {
var filterElement = filterList[i].parsed;
var negated = filterList[i].negated;
var sort = parseSortDirective_(filterElement, negated);
if (sort) {
this.sort = sort;
continue;
}
this.textWithoutSort += filterList[i].original;
var filter = parseRestrictDirective_(filterElement, negated);
if (!filter)
filter = parseStringDirective_(filterElement, negated);
if (filter) {
if (negated) {
filter = (function(func, sourceEntry) {
return !func(sourceEntry);
}).bind(null, filter);
}
filterFunctions.push(filter);
continue;
}
textFilters.push({ text: filterElement, negated: negated });
}
// Create a single filter for all text filters, so they can share a
// TabePrinter.
filterFunctions.push(textFilter_.bind(null, textFilters));
// Create function to go through all the filters.
this.filter = function(sourceEntry) {
for (var i = 0; i < filterFunctions.length; ++i) {
if (!filterFunctions[i](sourceEntry))
return false;
}
return true;
};
}
/**
* Parses a single "sort:" directive, and returns a dictionary containing
* the sort function and direction. Returns null on failure, including
* the case when no such sort function exists.
*/
function parseSortDirective_(filterElement, backwards) {
var match = /^sort:(.*)$/.exec(filterElement);
if (!match)
return null;
return { method: match[1], backwards: backwards };
}
/**
* Tries to parses |filterElement| as a single "is:" directive, and returns a
* new filter function. Returns null on failure.
*/
function parseRestrictDirective_(filterElement) {
var match = /^is:(.*)$/.exec(filterElement);
if (!match)
return null;
if (match[1] == 'active') {
return function(sourceEntry) { return !sourceEntry.isInactive(); };
}
if (match[1] == 'error') {
return function(sourceEntry) { return sourceEntry.isError(); };
}
return null;
}
/**
* Tries to parse |filterElement| as a single filter of a type that takes
* arbitrary strings as input, and returns a new filter function on success.
* Returns null on failure.
*/
function parseStringDirective_(filterElement) {
var match = RegExp('^([^:]*):(.*)$').exec(filterElement);
if (!match)
return null;
// Split parameters around commas and remove empty elements.
var parameters = match[2].split(',');
parameters = parameters.filter(function(string) {
return string.length > 0;
});
if (match[1] == 'type') {
return function(sourceEntry) {
var i;
var sourceType = sourceEntry.getSourceTypeString().toLowerCase();
for (i = 0; i < parameters.length; ++i) {
if (sourceType.search(parameters[i]) != -1)
return true;
}
return false;
};
}
if (match[1] == 'id') {
return function(sourceEntry) {
return parameters.indexOf(sourceEntry.getSourceId() + '') != -1;
};
}
return null;
}
/**
* Takes in the text of a filter and returns a list of
* {parsed, original, negated} values that correspond to substrings of the
* filter before and after filtering, and whether or not it started with a
* '-'. Extra whitespace other than a single character after each element is
* ignored. Parsed strings are all lowercase.
*/
function parseFilter_(filterText) {
// Assemble a list of quoted and unquoted strings in the filter.
var filterList = [];
var position = 0;
while (position < filterText.length) {
var inQuote = false;
var filterElement = '';
var negated = false;
var startPosition = position;
while (position < filterText.length) {
var nextCharacter = filterText[position];
++position;
if (nextCharacter == '\\' &&
position < filterText.length) {
// If there's a backslash, skip the backslash and add the next
// character to the element.
filterElement += filterText[position];
++position;
continue;
} else if (nextCharacter == '"') {
// If there's an unescaped quote character, toggle |inQuote| without
// modifying the element.
inQuote = !inQuote;
} else if (!inQuote && /\s/.test(nextCharacter)) {
// If not in a quote and have a whitespace character, that's the
// end of the element.
break;
} else if (nextCharacter == '-' && startPosition == position - 1) {
// If this is the first character, and it's a '-', this entry is
// negated.
negated = true;
} else {
// Otherwise, add the next character to the element.
filterElement += nextCharacter;
}
}
if (filterElement.length > 0) {
var filter = {
parsed: filterElement.toLowerCase(),
original: filterText.substring(startPosition, position),
negated: negated,
};
filterList.push(filter);
}
}
return filterList;
}
/**
* Takes in a list of text filters and a SourceEntry. Each filter has
* "text" and "negated" fields. Returns true if the SourceEntry matches all
* filters in the (possibly empty) list.
*/
function textFilter_(textFilters, sourceEntry) {
var tablePrinter = null;
for (var i = 0; i < textFilters.length; ++i) {
var text = textFilters[i].text;
var negated = textFilters[i].negated;
var match = false;
// The description is often not contained in one of the log entries.
// The source type almost never is, so check for them directly.
var description = sourceEntry.getDescription().toLowerCase();
var type = sourceEntry.getSourceTypeString().toLowerCase();
if (description.indexOf(text) != -1 || type.indexOf(text) != -1) {
match = true;
} else {
if (!tablePrinter) {
tablePrinter = createLogEntryTablePrinter(
sourceEntry.getLogEntries(),
SourceTracker.getInstance().getPrivacyStripping());
}
match = tablePrinter.search(text);
}
if (negated)
match = !match;
if (!match)
return false;
}
return true;
}
return SourceFilterParser;
})();
......@@ -91,7 +91,7 @@ var SourceRow = (function() {
this.updateDescription_();
// Update filters.
var matchesFilter = this.matchesFilter(this.parentView_.currentFilter_);
var matchesFilter = this.parentView_.currentFilter_(this.sourceEntry_);
this.setIsMatchedByFilter(matchesFilter);
},
......@@ -165,63 +165,6 @@ var SourceRow = (function() {
}
},
matchesFilter: function(filter) {
if (filter.isActive && this.isInactive_)
return false;
if (filter.isInactive && !this.isInactive_)
return false;
if (filter.isError && !this.isError_)
return false;
if (filter.isNotError && this.isError_)
return false;
// Check source type, if needed.
if (filter.type) {
var i;
var sourceType = this.sourceEntry_.getSourceTypeString().toLowerCase();
for (i = 0; i < filter.type.length; ++i) {
if (sourceType.search(filter.type[i]) != -1)
break;
}
if (i == filter.type.length)
return false;
}
// Check source ID, if needed.
if (filter.id) {
if (filter.id.indexOf(this.getSourceId() + '') == -1)
return false;
}
if (!filter.textFilters)
return true;
// Used for searching for input strings. Lazily initialized.
var tablePrinter = null;
for (var i = 0; i < filter.textFilters.length; ++i) {
// The description is not always contained in one of the log entries.
if (this.description_.toLowerCase().indexOf(
filter.textFilters[i]) != -1) {
continue;
}
// Allow specifying source types by name.
var sourceType = this.sourceEntry_.getSourceTypeString();
if (sourceType.toLowerCase().indexOf(filter.textFilters[i]) != -1)
continue;
if (!tablePrinter) {
tablePrinter = createLogEntryTablePrinter(
this.sourceEntry_.getLogEntries(),
SourceTracker.getInstance().getPrivacyStripping());
}
if (!tablePrinter.search(filter.textFilters[i]))
return false;
}
return true;
},
isSelected: function() {
return this.isSelected_;
},
......
......@@ -154,11 +154,14 @@ TEST_F('NetInternalsTest', 'netInternalsEventsViewFilter', function() {
{text: 'type:URL_REQUEST', matches: [true, true] },
{text: 'type:SOCKET,URL_REQUEST', matches: [true, true] },
{text: 'type:SOCKET', matches: [false, false] },
{text: '-type:PONY', matches: [true, true] },
{text: '-type:URL_REQUEST', matches: [false, false] },
{text: 'id:31,32', matches: [true, false] },
{text: '-id:31,32', matches: [false, true] },
{text: 'id:32,56,', matches: [false, true] },
{text: 'is:-active', matches: [true, false] },
{text: '-is:active', matches: [true, false] },
{text: 'is:active', matches: [false, true] },
{text: 'is:-error', matches: [true, true] },
{text: '-is:error', matches: [true, true] },
{text: 'is:error', matches: [false, false] },
// Partial match of source type.
{text: 'URL_REQ', matches: [true, true] },
......@@ -176,12 +179,13 @@ TEST_F('NetInternalsTest', 'netInternalsEventsViewFilter', function() {
{ text: '\\"\\"\\"\\"', matches: [false, false] },
{ text: '"Host: www.google.com"', matches: [true, false], },
{ text: 'Connection:" keep-alive"', matches: [true, false], },
{ text: '-Connection:" keep-alive"', matches: [false, true], },
{ text: '"Host: GET"', matches: [false, false] },
// Make sure sorting has no effect on filters. Sort by ID so order is
// preserved.
{ text: 'sort:"id"', matches: [true, true] },
// Sorting by unrecognized methods should do a text match.
{ text: 'sort:"shoe size"', matches: [false, false] },
{ text: '-sort:"shoe size"', matches: [true, true] },
{ text: '"-sort:shoe size"', matches: [false, false] },
];
for (var filter1 = 0; filter1 < testFilters.length; ++filter1) {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment