diff options
author | eroman@chromium.org <eroman@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-03-23 17:47:49 +0000 |
---|---|---|
committer | eroman@chromium.org <eroman@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-03-23 17:47:49 +0000 |
commit | fd9c0d9df665355c14d3c0df6f3c5b0b2059c129 (patch) | |
tree | 1d2c2cb1cd339587089ae0f913864ef163cf2778 /chrome/browser/resources/net_internals | |
parent | 3773020c131d144f0e0e1d849227b34d7c4cf6e1 (diff) | |
download | chromium_src-fd9c0d9df665355c14d3c0df6f3c5b0b2059c129.zip chromium_src-fd9c0d9df665355c14d3c0df6f3c5b0b2059c129.tar.gz chromium_src-fd9c0d9df665355c14d3c0df6f3c5b0b2059c129.tar.bz2 |
Add an initial implementation of net-internals inspector in javascript.
BUG=37421
Review URL: http://codereview.chromium.org/1088007
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@42357 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser/resources/net_internals')
-rw-r--r-- | chrome/browser/resources/net_internals/detailsview.js | 93 | ||||
-rw-r--r-- | chrome/browser/resources/net_internals/index.html | 87 | ||||
-rw-r--r-- | chrome/browser/resources/net_internals/layoutmanager.js | 151 | ||||
-rw-r--r-- | chrome/browser/resources/net_internals/loggrouper.js | 95 | ||||
-rw-r--r-- | chrome/browser/resources/net_internals/logviewpainter.js | 117 | ||||
-rw-r--r-- | chrome/browser/resources/net_internals/main.css | 134 | ||||
-rw-r--r-- | chrome/browser/resources/net_internals/main.js | 90 | ||||
-rw-r--r-- | chrome/browser/resources/net_internals/requestsview.js | 165 | ||||
-rw-r--r-- | chrome/browser/resources/net_internals/sourceentry.js | 182 | ||||
-rw-r--r-- | chrome/browser/resources/net_internals/timelineviewpainter.js | 20 | ||||
-rw-r--r-- | chrome/browser/resources/net_internals/util.js | 88 |
11 files changed, 1195 insertions, 27 deletions
diff --git a/chrome/browser/resources/net_internals/detailsview.js b/chrome/browser/resources/net_internals/detailsview.js new file mode 100644 index 0000000..271c19f --- /dev/null +++ b/chrome/browser/resources/net_internals/detailsview.js @@ -0,0 +1,93 @@ +// Copyright (c) 2010 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. + +/** + * The DetailsView handles the tabbed view that displays either the "log" or + * "timeline" view. This class keeps track of what the current view is, and + * invalidates the specific view each time the selected data has changed. + * + * @constructor + */ +function DetailsView(logTabHandleId, + timelineTabHandleId, + detailsTabContentId) { + // The DOM nodes that which contain the tab title. + this.tabHandles_ = {}; + + this.tabHandles_['timeline'] = document.getElementById(timelineTabHandleId); + this.tabHandles_['log'] = document.getElementById(logTabHandleId); + + // The DOM node that contains the currently active tab sheet. + this.contentArea_ = document.getElementById(detailsTabContentId); + + // Attach listeners to the "tab handles" so when you click them, it switches + // active view. + + var self = this; + + this.tabHandles_['timeline'].onclick = function() { + self.switchToView_('timeline'); + }; + + this.tabHandles_['log'].onclick = function() { + self.switchToView_('log'); + }; + + this.currentData_ = []; + + // Default to the log view. + this.switchToView_('log'); +}; + +// The delay between updates to repaint. +DetailsView.REPAINT_TIMEOUT_MS = 50; + +/** + * Switches to the tab with name |viewName|. (Either 'log' or 'timeline'. + */ +DetailsView.prototype.switchToView_ = function(viewName) { + if (this.currentView_) { + // Remove the selected styling on currently selected tab. + changeClassName(this.tabHandles_[this.currentView_], 'selected', false); + } + + this.currentView_ = viewName; + changeClassName(this.tabHandles_[this.currentView_], 'selected', true); + this.repaint_(); +}; + +/** + * Updates the data this view is using. + */ +DetailsView.prototype.setData = function(currentData) { + // Make a copy of the array (in case the caller mutates it), and sort it + // by the source ID. + this.currentData_ = DetailsView.createSortedCopy_(currentData); + + // Invalidate the view. + if (!this.outstandingRepaint_) { + this.outstandingRepaint_ = true; + window.setTimeout(this.repaint_.bind(this), + DetailsView.REPAINT_TIMEOUT_MS); + } +}; + +DetailsView.prototype.repaint_ = function() { + this.outstandingRepaint_ = false; + this.contentArea_.innerHTML = ''; + + if (this.currentView_ == 'log') { + PaintLogView(this.currentData_, this.contentArea_); + } else { + PaintTimelineView(this.currentData_, this.contentArea_); + } +}; + +DetailsView.createSortedCopy_ = function(origArray) { + var sortedArray = origArray.slice(0); + sortedArray.sort(function(a, b) { + return a.getSourceId() - b.getSourceId(); + }); + return sortedArray; +}; diff --git a/chrome/browser/resources/net_internals/index.html b/chrome/browser/resources/net_internals/index.html index 7aff02c..fab3304 100644 --- a/chrome/browser/resources/net_internals/index.html +++ b/chrome/browser/resources/net_internals/index.html @@ -1,32 +1,65 @@ <html> +<!-- +Copyright (c) 2010 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. +--> <head> - <title>Under construction...</title> - <script> - -// TODO(eroman): This is all temporary... - -function sendTestMessageToBrowser() { - log("Sent message to browser"); - chrome.send('testMessage', [String((new Date()).toLocaleTimeString())]); -} - -function log(msg) { - var l = document.getElementById('log'); - l.appendChild(document.createTextNode(msg + "\n")); -} - - </script> + <link tyle="text/css" rel="stylesheet" href="main.css" /> + <script src="main.js"></script> + <script src="util.js"></script> + <script src="requestsview.js"></script> + <script src="detailsview.js"></script> + <script src="sourceentry.js"></script> + <script src="layoutmanager.js"></script> + <script src="timelineviewpainter.js"></script> + <script src="logviewpainter.js"></script> + <script src="loggrouper.js"></script> </head> - - - <body> - <p>This is a work in progress. See http://crbug.com/37421 for details.</p> - - <input onclick="sendTestMessageToBrowser()" - value="SendTestMessageToBrowser" - type=button /> - - <pre id=log></pre> - + <body onload="onLoaded()"> + <!-- Filter Box: This the top bar which contains the search box. --> + <div id=filterBox> + <table width=100% height=100%> + <tr> + <td width=1%>Filter:</td> + <td width=98%><input type="search" incremental id=filterInput /></td> + <td width=1% id=filterCount>(1 of 34)</td> + </tr> + </table> + </div> + <!-- Requests Box: This the panel on the left which lists the requests --> + <div id=requestsBox> + <table id=requestsListTable cellspacing=0 cellpadding=0> + <thead> + <tr> + <td><input type=checkbox id=selectAll /></td> + <td>ID</td> + <td>Source</td> + <td>URL</td> + </tr> + </thead> + <!-- Requests table body: This is where request rows go into --> + <tbody id=requestsListTableBody></tbody> + </table> + </div> + <!-- Action Box: This is a button bar along the bottom --> + <div id=actionBox> + <input type=button value="Stop capturing" onclick="alert('TODO')" /> + <input type=button value="Delete selected" id=deleteSelected /> + </div> + <!-- Splitter Box: This is a handle to resize the vertical divider --> + <div id=splitterBox></div> + <!-- Details box: This is the panel on the right which shows information --> + <div id=detailsBox> + <table class=tabSwitcher cellspacing=0> + <tr> + <th id=detailsLogTabHandle>Log</th> + <td class=tabSwitcherSpacer> </td> + <th id=detailsTimelineTabHandle>Timeline</th> + </tr> + </table> + <div class=tabSwitcherLine></div> + <div id=detailsTabArea></div> + </div> </body> </html> diff --git a/chrome/browser/resources/net_internals/layoutmanager.js b/chrome/browser/resources/net_internals/layoutmanager.js new file mode 100644 index 0000000..0491a63 --- /dev/null +++ b/chrome/browser/resources/net_internals/layoutmanager.js @@ -0,0 +1,151 @@ +// Copyright (c) 2010 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. + +/** + * The class LayoutManager implements a vertically split layout that takes up + * the whole window, and provides a draggable bar to resize the left and right + * panes. Its elements are layed out as follows: + * + * + * <<-- sizer -->> + * + * +----------------------++----------------+ + * | topbar || | + * +----------------------+| | + * | || | + * | || | + * | || | + * | || | + * | middlebox || rightbox | + * | || | + * | || | + * | || | + * | || | + * | || | + * +----------------------++ | + * | bottombar || | + * +----------------------++----------------+ + * + * The "topbar" and "bottombar" have a fixed height which is determined + * by their initial content height. The rest of the boxes fill to occupy the + * remaining space. + * + * The consumer provides DIVs for each of these regions, and the LayoutManager + * positions them correctly in the window. + * + * @constructor + */ +function LayoutManager(topbarId, + middleboxId, + bottombarId, + sizerId, + rightboxId) { + // Lookup the elements. + this.topbar_ = document.getElementById(topbarId); + this.middlebox_ = document.getElementById(middleboxId); + this.bottombar_ = document.getElementById(bottombarId); + this.sizer_ = document.getElementById(sizerId); + this.rightbox_ = document.getElementById(rightboxId); + + // Make all the elements absolutely positioned. + this.topbar_.style.position = "absolute"; + this.middlebox_.style.position = "absolute"; + this.bottombar_.style.position = "absolute"; + this.sizer_.style.position = "absolute"; + this.rightbox_.style.position = "absolute"; + + // Set the initial split position at 50%. + setNodeWidth(this.rightbox_, window.innerWidth / 2); + + // Setup the "sizer" so it can be dragged left/right to reposition the + // vertical split. + this.sizer_.addEventListener("mousedown", this.onDragSizerStart_.bind(this), + true); + + // Recalculate the layout whenever the window size changes. + window.addEventListener("resize", this.recalculateLayout_.bind(this), true); + + // Do the initial layout . + this.recalculateLayout_(); +} + +// Minimum width to size panels to, in pixels. +LayoutManager.MIN_PANEL_WIDTH = 50; + +/** + * Repositions all of the elements to fit the window. + */ +LayoutManager.prototype.recalculateLayout_ = function() { + // Calculate the horizontal split points. + var totalWidth = window.innerWidth; + var rightboxWidth = this.rightbox_.offsetWidth; + var sizerWidth = this.sizer_.offsetWidth; + var leftPaneWidth = totalWidth - (rightboxWidth + sizerWidth); + + // Don't let the left pane get too small. + if (leftPaneWidth < LayoutManager.MIN_PANEL_WIDTH) { + leftPaneWidth = LayoutManager.MIN_PANEL_WIDTH; + rightboxWidth = totalWidth - (sizerWidth + leftPaneWidth); + } + + // Calculate the vertical split points. + var totalHeight = window.innerHeight; + var topbarHeight = this.topbar_.offsetHeight; + var bottombarHeight = this.bottombar_.offsetHeight; + var middleboxHeight = totalHeight - (topbarHeight + bottombarHeight); + + // Position the boxes using calculated split points. + setNodePosition(this.topbar_, 0, 0, + leftPaneWidth, topbarHeight); + setNodePosition(this.middlebox_, 0, topbarHeight, + leftPaneWidth, + middleboxHeight); + setNodePosition(this.bottombar_, 0, (topbarHeight + middleboxHeight), + leftPaneWidth, bottombarHeight); + + setNodePosition(this.sizer_, leftPaneWidth, 0, + sizerWidth, totalHeight); + setNodePosition(this.rightbox_, leftPaneWidth + sizerWidth, 0, + rightboxWidth, totalHeight); +}; + +/** + * Called once we have clicked into the sizer. Starts capturing the mouse + * position to implement dragging. + */ +LayoutManager.prototype.onDragSizerStart_ = function(event) { + this.sizerMouseMoveListener_ = this.onDragSizer.bind(this); + this.sizerMouseUpListener_ = this.onDragSizerEnd.bind(this); + + window.addEventListener("mousemove", this.sizerMouseMoveListener_, true); + window.addEventListener("mouseup", this.sizerMouseUpListener_, true); + + event.preventDefault(); +}; + +/** + * Called when the mouse has moved after dragging started. + */ +LayoutManager.prototype.onDragSizer = function(event) { + var newWidth = window.innerWidth - event.pageX; + + // Avoid shrinking the right box too much. + newWidth = Math.max(newWidth, LayoutManager.MIN_PANEL_WIDTH); + + setNodeWidth(this.rightbox_, newWidth); + this.recalculateLayout_(); +}; + +/** + * Called once the mouse has been released, and the dragging is over. + */ +LayoutManager.prototype.onDragSizerEnd = function(event) { + window.removeEventListener("mousemove", this.sizerMouseMoveListener_, true); + window.removeEventListener("mouseup", this.sizerMouseUpListener_, true); + + this.sizerMouseMoveListener_ = null; + this.sizerMouseUpListener_ = null; + + event.preventDefault(); +}; diff --git a/chrome/browser/resources/net_internals/loggrouper.js b/chrome/browser/resources/net_internals/loggrouper.js new file mode 100644 index 0000000..a3cf7a5 --- /dev/null +++ b/chrome/browser/resources/net_internals/loggrouper.js @@ -0,0 +1,95 @@ +// Copyright (c) 2010 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. + +/** + * LogGroupEntry is a wrapper around log entries, which makes it easier to + * find the corresponding start/end of events. + * + * This is used internally by the log and timeline views to pretty print + * collections of log entries. + * + * @fileoverview + */ + +// TODO(eroman): document these methods! + +function LogGroupEntry(origEntry, index) { + this.orig = origEntry; + this.index = index; +} + +LogGroupEntry.prototype.isBegin = function() { + return this.orig.type == LogEntryType.TYPE_EVENT && + this.orig.event.phase == LogEventPhase.PHASE_BEGIN; +}; + +LogGroupEntry.prototype.isEnd = function() { + return this.orig.type == LogEntryType.TYPE_EVENT && + this.orig.event.phase == LogEventPhase.PHASE_END +}; + +LogGroupEntry.prototype.getDepth = function() { + var depth = 0; + var p = this.parentEntry; + while (p) { + depth += 1; + p = p.parentEntry; + } + return depth; +}; + +function findParentIndex(parentStack, eventType) { + for (var i = parentStack.length - 1; i >= 0; --i) { + if (parentStack[i].orig.event.type == eventType) + return i; + } + return -1; +} + +/** + * Returns a list of LogGroupEntrys. This basically wraps the original log + * entry, but makes it easier to find the start/end of the event. + */ +LogGroupEntry.createArrayFrom = function(origEntries) { + var groupedEntries = []; + + // Stack of enclosing PHASE_BEGIN elements. + var parentStack = []; + + for (var i = 0; i < origEntries.length; ++i) { + var origEntry = origEntries[i]; + + var groupEntry = new LogGroupEntry(origEntry, i); + groupedEntries.push(groupEntry); + + // If this is the end of an event, match it to the start. + if (groupEntry.isEnd()) { + // Walk up the parent stack to find the corresponding BEGIN for this END. + var parentIndex = + findParentIndex(parentStack, groupEntry.orig.event.type); + + if (parentIndex == -1) { + // Unmatched end. + } else { + groupEntry.begin = parentStack[parentIndex]; + + // Consider this as the terminator for all open BEGINs up until + // parentIndex. + for (var j = 0; j < parentStack.length - parentIndex; ++j) { + var p = parentStack.pop(); + p.end = groupEntry; + } + } + } + + // Inherit the current parent. + if (parentStack.length > 0) + groupEntry.parentEntry = parentStack[parentStack.length - 1]; + + if (groupEntry.isBegin()) + parentStack.push(groupEntry); + } + + return groupedEntries; +} diff --git a/chrome/browser/resources/net_internals/logviewpainter.js b/chrome/browser/resources/net_internals/logviewpainter.js new file mode 100644 index 0000000..8a068f7 --- /dev/null +++ b/chrome/browser/resources/net_internals/logviewpainter.js @@ -0,0 +1,117 @@ +// Copyright (c) 2010 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. + +/** + * TODO(eroman): This needs better presentation, and cleaner code. This + * implementation is more of a transitionary step as + * the old net-internals is replaced. + */ + +var PaintLogView; + +// Start of anonymous namespace. +(function() { + +PaintLogView = function(sourceEntries, node) { + for (var i = 0; i < sourceEntries.length; ++i) { + if (i != 0) + addNode(node, 'hr'); + addSourceEntry_(node, sourceEntries[i]); + } +} + +const INDENTATION_PX = 20; + +function addSourceEntry_(node, sourceEntry) { + var div = addNode(node, 'div'); + div.className = 'logSourceEntry'; + + var p = addNode(div, 'p'); + var nobr = addNode(p, 'nobr'); + + addTextNode(nobr, sourceEntry.getDescription()); + + var groupedEntries = LogGroupEntry.createArrayFrom( + sourceEntry.getLogEntries()); + + makeLoadLogTable_(div, groupedEntries); +} + +function makeLoadLogTable_(node, entries) { + var table = addNode(node, 'table'); + var tbody = addNode(node, 'tbody'); + + for (var i = 0; i < entries.length; ++i) { + var entry = entries[i]; + + // Avoid printing the END for a BEGIN that was immediately before. + if (entry.isEnd() && entry.begin && entry.begin.index == i - 1) { + continue; + } + + var tr = addNode(node, 'tr'); + + var timeLabelCell = addNode(tr, 'td'); + addTextNode(timeLabelCell, 't='); + + var timeCell = addNode(tr, 'td'); + timeCell.style.textAlign = 'right'; + timeCell.style.paddingRight = '5px'; + addTextNode(timeCell, entry.orig.time); + + var mainCell = addNode(tr, 'td'); + mainCell.style.paddingRight = '5px'; + var dtLabelCell = addNode(tr, 'td'); + var dtCell = addNode(tr, 'td'); + dtCell.style.textAlign = 'right'; + + mainCell.style.paddingLeft = (INDENTATION_PX * entry.getDepth()) + "px"; + + if (entry.orig.type == LogEntryType.TYPE_EVENT) { + addTextNode(mainCell, getTextForEvent(entry)); + + // Get the elapsed time. + if (entry.isBegin()) { + addTextNode(dtLabelCell, '[dt='); + + // Definite time. + if (entry.end) { + var dt = entry.end.orig.time - entry.orig.time; + addTextNode(dtCell, dt + ']'); + } else { + addTextNode(dtCell, '?]'); + } + } + } else { + mainCell.colSpan = '3'; + if (entry.orig.type == LogEntryType.TYPE_STRING) { + addTextNode(mainCell, entry.orig.string); + } else if (entry.orig.type == LogEntryType.TYPE_ERROR_CODE) { + // TODO(eroman): print symbolic name of error code. + addTextNode(mainCell, "Network error: " + entry.orig.error_code); + } else { + addTextNode(mainCell, "Unrecognized entry type: " + entry.orig.type); + } + } + } +} + +function getTextForEvent(entry) { + var text = ''; + + if (entry.isBegin()) { + text = '+' + text; + } else if (entry.isEnd()) { + text = '-' + text; + } else { + text = '.'; + } + + text += getKeyWithValue(LogEventType, entry.orig.event.type); + return text; +} + +// End of anonymous namespace. +})(); + diff --git a/chrome/browser/resources/net_internals/main.css b/chrome/browser/resources/net_internals/main.css new file mode 100644 index 0000000..e6d6437 --- /dev/null +++ b/chrome/browser/resources/net_internals/main.css @@ -0,0 +1,134 @@ +/* +Copyright (c) 2010 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. +*/ + +* { + -webkit-box-sizing: border-box; +} +body { + font-family: sans-serif; +} + +#filterBox { + background: #efefef; + padding: 5px; + border-bottom: 1px solid gray; + overflow: hidden; +} + +#filterBox * { + white-space: nowrap; + font-family: sans-serif; + font-size: 12px; +} + +#filterBox input { + width: 100%; +} + +#actionBox { + background: #efefef; + white-space: nowrap; + border-top: 1px solid gray; + overflow: hidden; +} + +#requestsBox { + overflow-x: hidden; + overflow-y: auto +} + +#detailsBox { + overflow: auto; +} + +#splitterBox { + background: #bfbfbf; + border-left: 1px inset black; + border-right: 1px solid black; + position:absolute; + width: 8px; + cursor: col-resize; + user-select: none; +} + +#requestsListTable { + cursor: pointer; +} + +#requestsListTable thead td { + text-align: left; + font-weight: bold; + background: rgb(229, 236, 249); +} + +#requestsListTable td { + padding: 3px; + border-left: 1px solid #afafaf; + border-bottom: 1px solid #afafaf; + text-overflow: ellipsis; + font-size: 12px; + white-space: nowrap; +} + +#requestsListTableBody .mouseover { + background: rgb(244,244,255); +} + +#requestsListTableBody .selected, +#requestsListTableBody .mouseover .selected { + background: #C3D9FF; +} + +#requestsListTableBody .source_CONNECT_JOB { + color: blue; +} + +#requestsListTableBody .source_INIT_PROXY_RESOLVER { + color: orange; +} + +.tabSwitcher { + margin-top: 10px; + margin-left: 10px; +} + +.tabSwitcher th { + background: rgb(229,236,249); + cursor: pointer; + background-clip: border-box; + border-top-left-radius: 5px 5px; + border-top-right-radius: 5px 5px; + padding-left: 4px; + padding-top: 4px; + padding-right: 4px; + font-size: 12px; + margin-left: 30px; +} + +.tabSwitcher th.selected, .tabSwitcherLine { + background: rgb(195,217,255); +} + +.tabSwitcherLine { + height: 10px; +} + +#detailsTabArea { + margin-top: 10px; +} + +.logSourceEntry { + margin: 5px; +} + +.logSourceEntry * p { + font-weight: bold; + font-size: 12px; +} + +.logSourceEntry * td { + font-size: 10px; +} diff --git a/chrome/browser/resources/net_internals/main.js b/chrome/browser/resources/net_internals/main.js new file mode 100644 index 0000000..b23d71f --- /dev/null +++ b/chrome/browser/resources/net_internals/main.js @@ -0,0 +1,90 @@ +// Copyright (c) 2010 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. + +/** + * Dictionary of constants (initialized by browser). + */ +var LogEntryType = null; +var LogEventType = null; +var LogEventPhase = null; +var LogSourceType = null; + +/** + * Main entry point. called once the page has loaded. + */ +function onLoaded() { + // Layout the various DIVs in a vertically split fashion. + new LayoutManager("filterBox", + "requestsBox", + "actionBox", + "splitterBox", + "detailsBox"); + + // Create the view which displays information on the current selection. + var detailsView = new DetailsView("detailsLogTabHandle", + "detailsTimelineTabHandle", + "detailsTabArea"); + + // Create the view which displays requests lists, and lets you select, filter + // and delete them. + new RequestsView('requestsListTableBody', + 'filterInput', + 'filterCount', + 'deleteSelected', + 'selectAll', + detailsView); + + // Tell the browser that we are ready to start receiving log events. + notifyApplicationReady(); +} + +//------------------------------------------------------------------------------ +// Messages sent to the browser +//------------------------------------------------------------------------------ + +function notifyApplicationReady() { + chrome.send('notifyReady'); +} + +//------------------------------------------------------------------------------ +// Messages received from the browser +//------------------------------------------------------------------------------ + +function onLogEntryAdded(logEntry) { + LogDataProvider.broadcast(logEntry); +} + +function setLogEventTypeConstants(constantsMap) { + LogEventType = constantsMap; +} + +function setLogEventPhaseConstants(constantsMap) { + LogEventPhase = constantsMap; +} + +function setLogSourceTypeConstants(constantsMap) { + LogSourceType = constantsMap; +} + +function setLogEntryTypeConstants(constantsMap) { + LogEntryType = constantsMap; +} + +//------------------------------------------------------------------------------ +// LogDataProvider +//------------------------------------------------------------------------------ + +var LogDataProvider = {} + +LogDataProvider.observers_ = []; + +LogDataProvider.broadcast = function(logEntry) { + for (var i = 0; i < this.observers_.length; ++i) { + this.observers_[i].onLogEntryAdded(logEntry); + } +}; + +LogDataProvider.addObserver = function(observer) { + this.observers_.push(observer); +}; diff --git a/chrome/browser/resources/net_internals/requestsview.js b/chrome/browser/resources/net_internals/requestsview.js new file mode 100644 index 0000000..40707a4 --- /dev/null +++ b/chrome/browser/resources/net_internals/requestsview.js @@ -0,0 +1,165 @@ +// Copyright (c) 2010 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. + +/** + * RequestsView is the class which glues together the different components to + * form the primary UI: + * + * - The search filter + * - The requests table + * - The details panel + * - The action bar + * + * @constructor + */ +function RequestsView(tableBodyId, filterInputId, filterCountId, + deleteSelectedId, selectAllId, detailsView) { + this.sourceIdToEntryMap_ = {}; + this.currentSelectedSources_ = []; + + LogDataProvider.addObserver(this); + + this.tableBody_ = document.getElementById(tableBodyId); + this.detailsView_ = detailsView; + + this.filterInput_ = document.getElementById(filterInputId); + this.filterCount_ = document.getElementById(filterCountId); + + this.filterInput_.addEventListener("search", + this.onFilterTextChanged_.bind(this), true); + + document.getElementById(deleteSelectedId).onclick = + this.deleteSelected_.bind(this); + + document.getElementById(selectAllId).addEventListener( + 'click', this.selectAll_.bind(this), true); + + this.currentFilter_ = ''; + this.numPrefilter_ = 0; + this.numPostfilter_ = 0; + + this.invalidateFilterCounter_(); + this.invalidateDetailsView_(); +} + +// How soon after updating the filter list the counter should be updated. +RequestsView.REPAINT_FILTER_COUNTER_TIMEOUT_MS = 0; + +RequestsView.prototype.onFilterTextChanged_ = function() { + this.setFilter_(this.filterInput_.value); +}; + +RequestsView.prototype.setFilter_ = function(filterText) { + this.currentFilter_ = filterText; + + // Iterate through all of the rows and see if they match the filter. + for (var id in this.sourceIdToEntryMap_) { + var entry = this.sourceIdToEntryMap_[id]; + entry.setIsMatchedByFilter(entry.matchesFilter(this.currentFilter_)); + } +}; + +RequestsView.prototype.onLogEntryAdded = function(logEntry) { + // Lookup the source. + var sourceEntry = this.sourceIdToEntryMap_[logEntry.source.id]; + + if (!sourceEntry) { + sourceEntry = new SourceEntry(this); + this.sourceIdToEntryMap_[logEntry.source.id] = sourceEntry; + this.incrementPrefilterCount(1); + } + + sourceEntry.update(logEntry); + + if (sourceEntry.isSelected()) + this.invalidateDetailsView_(); +}; + +RequestsView.prototype.incrementPrefilterCount = function(offset) { + this.numPrefilter_ += offset; + this.invalidateFilterCounter_(); +}; + +RequestsView.prototype.incrementPostfilterCount = function(offset) { + this.numPostfilter_ += offset; + this.invalidateFilterCounter_(); +}; + +RequestsView.prototype.onSelectionChanged = function() { + this.invalidateDetailsView_(); +}; + +RequestsView.prototype.clearSelection = function() { + var prevSelection = this.currentSelectedSources_; + this.currentSelectedSources_ = []; + + // Unselect everything that is currently selected. + for (var i = 0; i < prevSelection.length; ++i) { + prevSelection[i].setSelected(false); + } + + this.onSelectionChanged(); +}; + +RequestsView.prototype.deleteSelected_ = function() { + var prevSelection = this.currentSelectedSources_; + this.currentSelectedSources_ = []; + + for (var i = 0; i < prevSelection.length; ++i) { + var entry = prevSelection[i]; + entry.remove(); + delete this.sourceIdToEntryMap_[entry.getSourceId()]; + this.incrementPrefilterCount(-1); + } +}; + +RequestsView.prototype.selectAll_ = function(event) { + for (var id in this.sourceIdToEntryMap_) { + var entry = this.sourceIdToEntryMap_[id]; + if (entry.isMatchedByFilter()) { + entry.setSelected(true); + } + } + event.preventDefault(); +}; + +RequestsView.prototype.modifySelectionArray = function( + sourceEntry, addToSelection) { + // Find the index for |sourceEntry| in the current selection list. + var index = -1; + for (var i = 0; i < this.currentSelectedSources_.length; ++i) { + if (this.currentSelectedSources_[i] == sourceEntry) { + index = i; + break; + } + } + + if (index != -1 && !addToSelection) { + // Remove from the selection. + this.currentSelectedSources_.splice(index, 1); + } + + if (index == -1 && addToSelection) { + this.currentSelectedSources_.push(sourceEntry); + } +} + +RequestsView.prototype.invalidateDetailsView_ = function() { + this.detailsView_.setData(this.currentSelectedSources_); +}; + +RequestsView.prototype.invalidateFilterCounter_ = function() { + if (!this.outstandingRepaintFilterCounter_) { + this.outstandingRepaintFilterCounter_ = true; + window.setTimeout(this.repaintFilterCounter_.bind(this), + RequestsView.REPAINT_FILTER_COUNTER_TIMEOUT_MS); + } +}; + +RequestsView.prototype.repaintFilterCounter_ = function() { + this.outstandingRepaintFilterCounter_ = false; + this.filterCount_.innerHTML = ''; + addTextNode(this.filterCount_, + this.numPostfilter_ + " of " + this.numPrefilter_); +}; diff --git a/chrome/browser/resources/net_internals/sourceentry.js b/chrome/browser/resources/net_internals/sourceentry.js new file mode 100644 index 0000000..72d3a44 --- /dev/null +++ b/chrome/browser/resources/net_internals/sourceentry.js @@ -0,0 +1,182 @@ +// Copyright (c) 2010 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. + +/** + * Each row in the filtered items list is backed by a SourceEntry. This + * instance contains all of the data pertaining to that row, and notifies + * its parent view (the RequestsView) whenever its data changes. + * + * @constructor + */ +function SourceEntry(parentView) { + this.entries_ = []; + this.parentView_ = parentView; + this.isSelected_ = false; + this.isMatchedByFilter_ = false; +} + +SourceEntry.prototype.isSelected = function() { + return this.isSelected_; +}; + +SourceEntry.prototype.setSelectedStyles = function(isSelected) { + changeClassName(this.row_, 'selected', isSelected); + this.getSelectionCheckbox().checked = isSelected; +}; + +SourceEntry.prototype.setMouseoverStyle = function(isMouseOver) { + changeClassName(this.row_, 'mouseover', isMouseOver); +}; + +SourceEntry.prototype.setIsMatchedByFilter = function(isMatchedByFilter) { + if (this.isMatchedByFilter() == isMatchedByFilter) + return; // No change. + + this.isMatchedByFilter_ = isMatchedByFilter; + + this.setFilterStyles(isMatchedByFilter); + + if (isMatchedByFilter) { + this.parentView_.incrementPostfilterCount(1); + } else { + this.parentView_.incrementPostfilterCount(-1); + // If we are filtering an entry away, make sure it is no longer + // part of the selection. + this.setSelected(false); + } +}; + +SourceEntry.prototype.isMatchedByFilter = function() { + return this.isMatchedByFilter_; +}; + +SourceEntry.prototype.setFilterStyles = function(isMatchedByFilter) { + // Hide rows which have been filtered away. + if (isMatchedByFilter) { + this.row_.style.display = ''; + } else { + this.row_.style.display = 'none'; + } +}; + +SourceEntry.prototype.update = function(logEntry) { + this.entries_.push(logEntry); + + if (this.entries_.length == 1) { + this.createRow_(); + + // Only apply the filter during the first update. + // TODO(eroman): once filters use other data, apply it on each update. + var matchesFilter = this.matchesFilter(this.parentView_.currentFilter_); + this.setIsMatchedByFilter(matchesFilter); + } +}; + +SourceEntry.prototype.onCheckboxToggled_ = function() { + this.setSelected(this.getSelectionCheckbox().checked); +}; + +SourceEntry.prototype.matchesFilter = function(filterText) { + // TODO(eroman): Support more advanced filter syntax. + if (filterText == '') + return true; + + var filterText = filterText.toLowerCase(); + + return this.getDescription().toLowerCase().indexOf(filterText) != -1 || + this.getSourceTypeString().toLowerCase().indexOf(filterText) != -1; +}; + +SourceEntry.prototype.setSelected = function(isSelected) { + if (isSelected == this.isSelected()) + return; + + this.isSelected_ = isSelected; + + this.setSelectedStyles(isSelected); + this.parentView_.modifySelectionArray(this, isSelected); + this.parentView_.onSelectionChanged(); +}; + +SourceEntry.prototype.onClicked_ = function() { + this.parentView_.clearSelection(); + this.setSelected(true); +}; + +SourceEntry.prototype.onMouseover_ = function() { + this.setMouseoverStyle(true); +}; + +SourceEntry.prototype.onMouseout_ = function() { + this.setMouseoverStyle(false); +}; + +SourceEntry.prototype.createRow_ = function() { + // Create a row. + var tr = addNode(this.parentView_.tableBody_, 'tr'); + tr.style.display = 'none'; + this.row_ = tr; + + var selectionCol = addNode(tr, 'td'); + var checkbox = addNode(selectionCol, 'input'); + checkbox.type = 'checkbox'; + + var idCell = addNode(tr, 'td'); + var typeCell = addNode(tr, 'td'); + var descriptionCell = addNode(tr, 'td'); + + // Connect listeners. + checkbox.onchange = this.onCheckboxToggled_.bind(this); + + var onclick = this.onClicked_.bind(this); + idCell.onclick = onclick; + typeCell.onclick = onclick; + descriptionCell.onclick = onclick; + + tr.onmouseover = this.onMouseover_.bind(this); + tr.onmouseout = this.onMouseout_.bind(this); + + // Set the cell values to match this source's data. + addTextNode(idCell, this.getSourceId()); + var sourceTypeString = this.getSourceTypeString(); + addTextNode(typeCell, sourceTypeString); + addTextNode(descriptionCell, this.getDescription()); + + // Add a CSS classname specific to this source type (so CSS can specify + // different stylings for different types). + changeClassName(this.row_, "source_" + sourceTypeString, true); +}; + +SourceEntry.prototype.getDescription = function() { + var e = this.entries_[0]; + if (e.type == LogEntryType.TYPE_EVENT && + e.event.phase == LogEventPhase.PHASE_BEGIN && + e.string != undefined) { + return e.string; // The URL / hostname / whatever. + } + return ''; +}; + +SourceEntry.prototype.getLogEntries = function() { + return this.entries_; +}; + +SourceEntry.prototype.getSourceTypeString = function() { + return getKeyWithValue(LogSourceType, this.entries_[0].source.type); +}; + +SourceEntry.prototype.getSelectionCheckbox = function() { + return this.row_.childNodes[0].firstChild; +}; + +SourceEntry.prototype.getSourceId = function() { + return this.entries_[0].source.id; +}; + +SourceEntry.prototype.remove = function() { + this.setSelected(false); + this.setIsMatchedByFilter(false); + this.row_.parentNode.removeChild(this.row_); +}; + diff --git a/chrome/browser/resources/net_internals/timelineviewpainter.js b/chrome/browser/resources/net_internals/timelineviewpainter.js new file mode 100644 index 0000000..2c7fb046 --- /dev/null +++ b/chrome/browser/resources/net_internals/timelineviewpainter.js @@ -0,0 +1,20 @@ +// Copyright (c) 2010 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 PaintTimelineView; + +(function() { + +PaintTimelineView = function(sourceEntries, node) { + addTextNode(node, 'TODO(eroman): Draw some sort of waterfall.'); + + addNode(node, 'br'); + addNode(node, 'br'); + + addTextNode(node, 'Selected nodes (' + sourceEntries.length + '):'); + addNode(node, 'br'); + +} + +})(); diff --git a/chrome/browser/resources/net_internals/util.js b/chrome/browser/resources/net_internals/util.js new file mode 100644 index 0000000..780a030 --- /dev/null +++ b/chrome/browser/resources/net_internals/util.js @@ -0,0 +1,88 @@ +// Copyright (c) 2010 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. + +/** + * Helper that binds the |this| object to a method to create a callback. + */ +Function.prototype.bind = function(thisObj) { + var func = this; + var args = Array.prototype.slice.call(arguments, 1); + return function() { + return func.apply(thisObj, + args.concat(Array.prototype.slice.call(arguments, 0))) + }; +}; + +/** + * Sets the width (in pixels) on a DOM node. + */ +function setNodeWidth(node, widthPx) { + node.style.width = widthPx.toFixed(0) + "px"; +} + +/** + * Sets the height (in pixels) on a DOM node. + */ +function setNodeHeight(node, heightPx) { + node.style.height = heightPx.toFixed(0) + "px"; +} + +/** + * Sets the position and size of a DOM node (in pixels). + */ +function setNodePosition(node, leftPx, topPx, widthPx, heightPx) { + node.style.left = leftPx.toFixed(0) + "px"; + node.style.top = topPx.toFixed(0) + "px"; + setNodeWidth(node, widthPx); + setNodeHeight(node, heightPx); +} + +/** + * Adds a node to |parentNode|, of type |tagName|. + */ +function addNode(parentNode, tagName) { + var elem = parentNode.ownerDocument.createElement(tagName); + parentNode.appendChild(elem); + return elem; +} + +/** + * Adds text to node |parentNode|. + */ +function addTextNode(parentNode, text) { + var textNode = parentNode.ownerDocument.createTextNode(text); + parentNode.appendChild(textNode); + return textNode; +} + +/** + * Adds or removes a CSS class to |node|. + */ +function changeClassName(node, classNameToAddOrRemove, isAdd) { + // Multiple classes can be separated by spaces. + var currentNames = node.className.split(" "); + + if (isAdd) { + if (!(classNameToAddOrRemove in currentNames)) { + currentNames.push(classNameToAddOrRemove); + } + } else { + for (var i = 0; i < currentNames.length; ++i) { + if (currentNames[i] == classNameToAddOrRemove) { + currentNames.splice(i, 1); + break; + } + } + } + + node.className = currentNames.join(" "); +} + +function getKeyWithValue(map, value) { + for (key in map) { + if (map[key] == value) + return key; + } + return '?'; +} |