Files
react-native-devtools-frontend/front_end/network/ResourceWebSocketFrameView.js
T
Tim van der Lippe 70842f313f Replace title property setter with Tooltip install invocation
ui/Tooltip.js overrides the HTMLElement prototype to override the
title property. Rather than using this property, we shoul be
calling Tooltip.install directly. This makes sure that new
components are not relying on the behavior of the legacy
prototype patching.

These usages have been manually audited using the following regexes:

Search: ([\S]+)\.title = ([^;]+);
Replace: UI.Tooltip.Tooltip.install($1, $2);

Note that there are classes in DevTools that also have a title
property. Most notably `TreeElement`. We should not be replacing
these, as they do not inherit from HTMLElement. Luckily, we are
running TypeScript to make sure we don't call `Tooltip.install`
with a non-HTMLElement.

A follow-up CL will clean up the getters.

R=jacktfranklin@chromium.org

Bug: 1150762
No-presubmit: True
Change-Id: I5928e75c70293531849e0576f4fb2a2a8b3e02d2
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2555060
Commit-Queue: Tim van der Lippe <tvanderlippe@chromium.org>
Auto-Submit: Tim van der Lippe <tvanderlippe@chromium.org>
Reviewed-by: Jack Franklin <jacktfranklin@chromium.org>
2020-11-23 17:16:46 +00:00

388 lines
13 KiB
JavaScript

/*
* Copyright (C) 2012 Research In Motion Limited. All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
import * as Common from '../common/common.js';
import * as DataGrid from '../data_grid/data_grid.js';
import * as Host from '../host/host.js';
import * as Platform from '../platform/platform.js';
import * as SDK from '../sdk/sdk.js';
import * as SourceFrame from '../source_frame/source_frame.js';
import * as TextUtils from '../text_utils/text_utils.js';
import * as UI from '../ui/ui.js';
import {BinaryResourceView} from './BinaryResourceView.js';
/**
* @unrestricted
*/
export class ResourceWebSocketFrameView extends UI.Widget.VBox {
/**
* @param {!SDK.NetworkRequest.NetworkRequest} request
*/
constructor(request) {
super();
this.registerRequiredCSS('network/webSocketFrameView.css', {enableLegacyPatching: true});
this.element.classList.add('websocket-frame-view');
this._request = request;
this._splitWidget = new UI.SplitWidget.SplitWidget(false, true, 'resourceWebSocketFrameSplitViewState');
this._splitWidget.show(this.element);
const columns = /** @type {!Array<!DataGrid.DataGrid.ColumnDescriptor>} */ ([
{id: 'data', title: Common.UIString.UIString('Data'), sortable: false, weight: 88}, {
id: 'length',
title: Common.UIString.UIString('Length'),
sortable: false,
align: DataGrid.DataGrid.Align.Right,
weight: 5
},
{id: 'time', title: Common.UIString.UIString('Time'), sortable: true, weight: 7}
]);
this._dataGrid = new DataGrid.SortableDataGrid.SortableDataGrid({
displayName: ls`Web Socket Frame`,
columns,
editCallback: undefined,
deleteCallback: undefined,
refreshCallback: undefined
});
this._dataGrid.setRowContextMenuCallback(onRowContextMenu.bind(this));
this._dataGrid.setStickToBottom(true);
this._dataGrid.setCellClass('websocket-frame-view-td');
this._timeComparator =
/** @type {function(!DataGrid.SortableDataGrid.SortableDataGridNode<!ResourceWebSocketFrameNode>, !DataGrid.SortableDataGrid.SortableDataGridNode<!ResourceWebSocketFrameNode>):number} */
(ResourceWebSocketFrameNodeTimeComparator);
this._dataGrid.sortNodes(this._timeComparator, false);
this._dataGrid.markColumnAsSortedBy('time', DataGrid.DataGrid.Order.Ascending);
this._dataGrid.addEventListener(DataGrid.DataGrid.Events.SortingChanged, this._sortItems, this);
this._dataGrid.setName('ResourceWebSocketFrameView');
this._dataGrid.addEventListener(DataGrid.DataGrid.Events.SelectedNode, event => {
this._onFrameSelected(event);
}, this);
this._dataGrid.addEventListener(DataGrid.DataGrid.Events.DeselectedNode, this._onFrameDeselected, this);
this._mainToolbar = new UI.Toolbar.Toolbar('');
this._clearAllButton = new UI.Toolbar.ToolbarButton(Common.UIString.UIString('Clear All'), 'largeicon-clear');
this._clearAllButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, this._clearFrames, this);
this._mainToolbar.appendToolbarItem(this._clearAllButton);
this._filterTypeCombobox = new UI.Toolbar.ToolbarComboBox(this._updateFilterSetting.bind(this), ls`Filter`);
for (const filterItem of _filterTypes) {
const option = this._filterTypeCombobox.createOption(filterItem.label, filterItem.name);
this._filterTypeCombobox.addOption(option);
}
this._mainToolbar.appendToolbarItem(this._filterTypeCombobox);
this._filterType = null;
const placeholder = 'Enter regex, for example: (web)?socket';
this._filterTextInput = new UI.Toolbar.ToolbarInput(Common.UIString.UIString(placeholder), '', 0.4);
this._filterTextInput.addEventListener(UI.Toolbar.ToolbarInput.Event.TextChanged, this._updateFilterSetting, this);
this._mainToolbar.appendToolbarItem(this._filterTextInput);
this._filterRegex = null;
const mainContainer = new UI.Widget.VBox();
mainContainer.element.appendChild(this._mainToolbar.element);
this._dataGrid.asWidget().show(mainContainer.element);
mainContainer.setMinimumSize(0, 72);
this._splitWidget.setMainWidget(mainContainer);
this._frameEmptyWidget =
new UI.EmptyWidget.EmptyWidget(Common.UIString.UIString('Select message to browse its content.'));
this._splitWidget.setSidebarWidget(this._frameEmptyWidget);
/** @type {?ResourceWebSocketFrameNode} */
this._selectedNode = null;
/**
* @param {!UI.ContextMenu.ContextMenu} contextMenu
* @param {!DataGrid.DataGrid.DataGridNode<?>} genericNode
* @this {ResourceWebSocketFrameView}
*/
function onRowContextMenu(contextMenu, genericNode) {
const node = /** @type {!ResourceWebSocketFrameNode} */ (genericNode);
const binaryView = node.binaryView();
if (binaryView) {
binaryView.addCopyToContextMenu(contextMenu, ls`Copy message...`);
} else {
contextMenu.clipboardSection().appendItem(
Common.UIString.UIString('Copy message'),
Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText.bind(
Host.InspectorFrontendHost.InspectorFrontendHostInstance, node.data.data));
}
contextMenu.footerSection().appendItem(Common.UIString.UIString('Clear all'), this._clearFrames.bind(this));
}
}
/**
* @param {number} opCode
* @param {boolean} mask
* @return {string}
*/
static opCodeDescription(opCode, mask) {
const localizedDescription = opCodeDescriptions[opCode] || '';
if (mask) {
return ls`${localizedDescription} (Opcode ${opCode}, mask)`;
}
return ls`${localizedDescription} (Opcode ${opCode})`;
}
/**
* @override
*/
wasShown() {
this.refresh();
this._request.addEventListener(SDK.NetworkRequest.Events.WebsocketFrameAdded, this._frameAdded, this);
}
/**
* @override
*/
willHide() {
this._request.removeEventListener(SDK.NetworkRequest.Events.WebsocketFrameAdded, this._frameAdded, this);
}
/**
* @param {!Common.EventTarget.EventTargetEvent} event
*/
_frameAdded(event) {
const frame = /** @type {!SDK.NetworkRequest.WebSocketFrame} */ (event.data);
if (!this._frameFilter(frame)) {
return;
}
this._dataGrid.insertChild(new ResourceWebSocketFrameNode(this._request.url(), frame));
}
/**
* @param {!SDK.NetworkRequest.WebSocketFrame} frame
* @return {boolean}
*/
_frameFilter(frame) {
if (this._filterType && frame.type !== this._filterType) {
return false;
}
return !this._filterRegex || this._filterRegex.test(frame.text);
}
_clearFrames() {
// TODO(allada): actially remove frames from request.
_clearFrameOffsets.set(this._request, this._request.frames().length);
this.refresh();
}
_updateFilterSetting() {
const text = this._filterTextInput.value();
const type = /** @type {!HTMLOptionElement} */ (this._filterTypeCombobox.selectedOption()).value;
this._filterRegex = text ? new RegExp(text, 'i') : null;
this._filterType = type === 'all' ? null : type;
this.refresh();
}
/**
* @param {!Common.EventTarget.EventTargetEvent} event
*/
async _onFrameSelected(event) {
this._currentSelectedNode = /** @type {!ResourceWebSocketFrameNode} */ (event.data);
const content = this._currentSelectedNode.dataText();
const binaryView = this._currentSelectedNode.binaryView();
if (binaryView) {
this._splitWidget.setSidebarWidget(binaryView);
return;
}
const jsonView = await SourceFrame.JSONView.JSONView.createView(content);
if (jsonView) {
this._splitWidget.setSidebarWidget(jsonView);
return;
}
this._splitWidget.setSidebarWidget(new SourceFrame.ResourceSourceFrame.ResourceSourceFrame(
TextUtils.StaticContentProvider.StaticContentProvider.fromString(
this._request.url(), Common.ResourceType.resourceTypes.WebSocket, content)));
}
/**
* @param {!Common.EventTarget.EventTargetEvent} event
*/
_onFrameDeselected(event) {
this._currentSelectedNode = null;
this._splitWidget.setSidebarWidget(this._frameEmptyWidget);
}
refresh() {
this._dataGrid.rootNode().removeChildren();
const url = this._request.url();
let frames = this._request.frames();
const offset = _clearFrameOffsets.get(this._request) || 0;
frames = frames.slice(offset);
frames = frames.filter(this._frameFilter.bind(this));
frames.forEach(frame => this._dataGrid.insertChild(new ResourceWebSocketFrameNode(url, frame)));
}
_sortItems() {
this._dataGrid.sortNodes(this._timeComparator, !this._dataGrid.isSortOrderAscending());
}
}
/** @enum {number} */
export const OpCodes = {
ContinuationFrame: 0,
TextFrame: 1,
BinaryFrame: 2,
ConnectionCloseFrame: 8,
PingFrame: 9,
PongFrame: 10
};
/** @type {!Array.<string> } */
export const opCodeDescriptions = (function() {
const opCodes = OpCodes;
const map = [];
map[opCodes.ContinuationFrame] = ls`Continuation Frame`;
map[opCodes.TextFrame] = ls`Text Message`;
map[opCodes.BinaryFrame] = ls`Binary Message`;
map[opCodes.ContinuationFrame] = ls`Connection Close Message`;
map[opCodes.PingFrame] = ls`Ping Message`;
map[opCodes.PongFrame] = ls`Pong Message`;
return map;
})();
/** @type {!Array<!UI.FilterBar.Item>} */
export const _filterTypes = [
{name: 'all', label: Common.UIString.UIString('All'), title: undefined},
{name: 'send', label: Common.UIString.UIString('Send'), title: undefined},
{name: 'receive', label: Common.UIString.UIString('Receive'), title: undefined},
];
/**
* @unrestricted
* @extends {DataGrid.SortableDataGrid.SortableDataGridNode<?>}
*/
export class ResourceWebSocketFrameNode extends DataGrid.SortableDataGrid.SortableDataGridNode {
/**
* @param {string} url
* @param {!SDK.NetworkRequest.WebSocketFrame} frame
*/
constructor(url, frame) {
let length = String(frame.text.length);
const time = new Date(frame.time * 1000);
const timeText = ('0' + time.getHours()).substr(-2) + ':' + ('0' + time.getMinutes()).substr(-2) + ':' +
('0' + time.getSeconds()).substr(-2) + '.' + ('00' + time.getMilliseconds()).substr(-3);
const timeNode = document.createElement('div');
UI.UIUtils.createTextChild(timeNode, timeText);
UI.Tooltip.Tooltip.install(timeNode, time.toLocaleString());
let dataText = frame.text;
let description = ResourceWebSocketFrameView.opCodeDescription(frame.opCode, frame.mask);
const isTextFrame = frame.opCode === OpCodes.TextFrame;
if (frame.type === SDK.NetworkRequest.WebSocketFrameType.Error) {
description = dataText;
length = ls`N/A`;
} else if (isTextFrame) {
description = dataText;
} else if (frame.opCode === OpCodes.BinaryFrame) {
length = Platform.NumberUtilities.bytesToString(base64ToSize(frame.text));
description = opCodeDescriptions[frame.opCode];
} else {
dataText = description;
}
super({data: description, length: length, time: timeNode});
this._url = url;
this._frame = frame;
this._isTextFrame = isTextFrame;
this._dataText = dataText;
/** @type {?BinaryResourceView} */
this._binaryView = null;
}
/**
* @override
* @param {!Element} element
*/
createCells(element) {
element.classList.toggle(
'websocket-frame-view-row-error', this._frame.type === SDK.NetworkRequest.WebSocketFrameType.Error);
element.classList.toggle(
'websocket-frame-view-row-send', this._frame.type === SDK.NetworkRequest.WebSocketFrameType.Send);
element.classList.toggle(
'websocket-frame-view-row-receive', this._frame.type === SDK.NetworkRequest.WebSocketFrameType.Receive);
super.createCells(element);
}
/**
* @override
* @return {number}
*/
nodeSelfHeight() {
return 21;
}
/**
* @return {string}
*/
dataText() {
return this._dataText;
}
/**
* @return {!OpCodes}
*/
opCode() {
return /** @type {!OpCodes} */ (this._frame.opCode);
}
/**
* @return {?BinaryResourceView}
*/
binaryView() {
if (this._isTextFrame || this._frame.type === SDK.NetworkRequest.WebSocketFrameType.Error) {
return null;
}
if (!this._binaryView) {
if (this.dataText.length > 0) {
this._binaryView =
new BinaryResourceView(this._dataText, /* url */ '', Common.ResourceType.resourceTypes.WebSocket);
}
}
return this._binaryView;
}
}
/**
* @param {!ResourceWebSocketFrameNode} a
* @param {!ResourceWebSocketFrameNode} b
* @return {number}
*/
export function ResourceWebSocketFrameNodeTimeComparator(a, b) {
return a._frame.time - b._frame.time;
}
/** @type {!WeakMap<!SDK.NetworkRequest.NetworkRequest, number>} */
const _clearFrameOffsets = new WeakMap();