blob: 9f41f64997febbe717b020d5ade758cbe424e3de [file] [log] [blame]
// Copyright 2021 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.
/*
* 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 '../../core/common/common.js';
import * as Host from '../../core/host/host.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as Platform from '../../core/platform/platform.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as TextUtils from '../../models/text_utils/text_utils.js';
import * as DataGrid from '../../ui/legacy/components/data_grid/data_grid.js';
import * as SourceFrame from '../../ui/legacy/components/source_frame/source_frame.js';
import * as UI from '../../ui/legacy/legacy.js';
import {BinaryResourceView} from './BinaryResourceView.js';
import webSocketFrameViewStyles from './webSocketFrameView.css.js';
const UIStrings = {
/**
*@description Text in Event Source Messages View of the Network panel
*/
data: 'Data',
/**
*@description Text in Resource Web Socket Frame View of the Network panel
*/
length: 'Length',
/**
*@description Text that refers to the time
*/
time: 'Time',
/**
*@description Data grid name for Web Socket Frame data grids
*/
webSocketFrame: 'Web Socket Frame',
/**
*@description Text to clear everything
*/
clearAll: 'Clear All',
/**
*@description Text to filter result items
*/
filter: 'Filter',
/**
*@description Text in Resource Web Socket Frame View of the Network panel
*/
selectMessageToBrowseItsContent: 'Select message to browse its content.',
/**
*@description Text in Resource Web Socket Frame View of the Network panel
*/
copyMessageD: 'Copy message...',
/**
*@description A context menu item in the Resource Web Socket Frame View of the Network panel
*/
copyMessage: 'Copy message',
/**
*@description Text to clear everything
*/
clearAllL: 'Clear all',
/**
* @description Text in Resource Web Socket Frame View of the Network panel. Displays which Opcode
* is relevant to a particular operation. 'mask' indicates that the Opcode used a mask, which is a
* way of modifying a value by overlaying another value on top of it, partially covering/changing
* it, hence 'masking' it.
* https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers
* @example {Localized name of the Opcode} PH1
* @example {0} PH2
*/
sOpcodeSMask: '{PH1} (Opcode {PH2}, mask)',
/**
* @description Text in Resource Web Socket Frame View of the Network panel. Displays which Opcode
* is relevant to a particular operation.
* @example {Localized name of the Opcode} PH1
* @example {0} PH2
*/
sOpcodeS: '{PH1} (Opcode {PH2})',
/**
*@description Op codes continuation frame of map in Resource Web Socket Frame View of the Network panel
*/
continuationFrame: 'Continuation Frame',
/**
*@description Op codes text frame of map in Resource Web Socket Frame View of the Network panel
*/
textMessage: 'Text Message',
/**
*@description Op codes binary frame of map in Resource Web Socket Frame View of the Network panel
*/
binaryMessage: 'Binary Message',
/**
*@description Op codes continuation frame of map in Resource Web Socket Frame View of the Network panel indicating that the web socket connection has been closed.
*/
connectionCloseMessage: 'Connection Close Message',
/**
*@description Op codes ping frame of map in Resource Web Socket Frame View of the Network panel
*/
pingMessage: 'Ping Message',
/**
*@description Op codes pong frame of map in Resource Web Socket Frame View of the Network panel
*/
pongMessage: 'Pong Message',
/**
*@description Text for everything
*/
all: 'All',
/**
*@description Text in Resource Web Socket Frame View of the Network panel
*/
send: 'Send',
/**
*@description Text in Resource Web Socket Frame View of the Network panel
*/
receive: 'Receive',
/**
*@description Text for something not available
*/
na: 'N/A',
/**
*@description Example for placeholder text
*/
enterRegex: 'Enter regex, for example: (web)?socket',
};
const str_ = i18n.i18n.registerUIStrings('panels/network/ResourceWebSocketFrameView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
const i18nLazyString = i18n.i18n.getLazilyComputedLocalizedString.bind(undefined, str_);
export class ResourceWebSocketFrameView extends UI.Widget.VBox {
private readonly request: SDK.NetworkRequest.NetworkRequest;
private readonly splitWidget: UI.SplitWidget.SplitWidget;
private dataGrid: DataGrid.SortableDataGrid.SortableDataGrid<unknown>;
private readonly timeComparator:
(arg0: DataGrid.SortableDataGrid.SortableDataGridNode<ResourceWebSocketFrameNode>,
arg1: DataGrid.SortableDataGrid.SortableDataGridNode<ResourceWebSocketFrameNode>) => number;
private readonly mainToolbar: UI.Toolbar.Toolbar;
private readonly clearAllButton: UI.Toolbar.ToolbarButton;
private readonly filterTypeCombobox: UI.Toolbar.ToolbarComboBox;
private filterType: string|null;
private readonly filterTextInput: UI.Toolbar.ToolbarInput;
private filterRegex: RegExp|null;
private readonly frameEmptyWidget: UI.EmptyWidget.EmptyWidget;
private readonly selectedNode: ResourceWebSocketFrameNode|null;
private currentSelectedNode?: ResourceWebSocketFrameNode|null;
private messageFilterSetting: Common.Settings.Setting<string> =
Common.Settings.Settings.instance().createSetting('networkWebSocketMessageFilter', '');
constructor(request: SDK.NetworkRequest.NetworkRequest) {
super();
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 = ([
{id: 'data', title: i18nString(UIStrings.data), sortable: false, weight: 88},
{
id: 'length',
title: i18nString(UIStrings.length),
sortable: false,
align: DataGrid.DataGrid.Align.Right,
weight: 5,
},
{id: 'time', title: i18nString(UIStrings.time), sortable: true, weight: 7},
] as DataGrid.DataGrid.ColumnDescriptor[]);
this.dataGrid = new DataGrid.SortableDataGrid.SortableDataGrid({
displayName: i18nString(UIStrings.webSocketFrame),
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 =
(ResourceWebSocketFrameNodeTimeComparator as
(arg0: DataGrid.SortableDataGrid.SortableDataGridNode<ResourceWebSocketFrameNode>,
arg1: DataGrid.SortableDataGrid.SortableDataGridNode<ResourceWebSocketFrameNode>) => number);
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 => {
void 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(i18nString(UIStrings.clearAll), '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), i18nString(UIStrings.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 = i18nString(UIStrings.enterRegex);
this.filterTextInput = new UI.Toolbar.ToolbarInput(placeholder, '', 0.4);
this.filterTextInput.addEventListener(UI.Toolbar.ToolbarInput.Event.TextChanged, this.updateFilterSetting, this);
const filter = this.messageFilterSetting.get();
if (filter) {
this.filterTextInput.setValue(filter);
}
this.filterRegex = null;
this.mainToolbar.appendToolbarItem(this.filterTextInput);
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(i18nString(UIStrings.selectMessageToBrowseItsContent));
this.splitWidget.setSidebarWidget(this.frameEmptyWidget);
this.selectedNode = null;
if (filter) {
this.applyFilter(filter);
}
function onRowContextMenu(
this: ResourceWebSocketFrameView, contextMenu: UI.ContextMenu.ContextMenu,
genericNode: DataGrid.DataGrid.DataGridNode<unknown>): void {
const node = (genericNode as ResourceWebSocketFrameNode);
const binaryView = node.binaryView();
if (binaryView) {
binaryView.addCopyToContextMenu(contextMenu, i18nString(UIStrings.copyMessageD));
} else {
contextMenu.clipboardSection().appendItem(
i18nString(UIStrings.copyMessage),
Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText.bind(
Host.InspectorFrontendHost.InspectorFrontendHostInstance, node.data.data));
}
contextMenu.footerSection().appendItem(i18nString(UIStrings.clearAllL), this.clearFrames.bind(this));
}
}
static opCodeDescription(opCode: number, mask: boolean): string {
const localizedDescription = opCodeDescriptions[opCode] || ((): string => '');
if (mask) {
return i18nString(UIStrings.sOpcodeSMask, {PH1: localizedDescription(), PH2: opCode});
}
return i18nString(UIStrings.sOpcodeS, {PH1: localizedDescription(), PH2: opCode});
}
wasShown(): void {
this.refresh();
this.registerCSSFiles([webSocketFrameViewStyles]);
this.request.addEventListener(SDK.NetworkRequest.Events.WebsocketFrameAdded, this.frameAdded, this);
}
willHide(): void {
this.request.removeEventListener(SDK.NetworkRequest.Events.WebsocketFrameAdded, this.frameAdded, this);
}
private frameAdded(event: Common.EventTarget.EventTargetEvent<SDK.NetworkRequest.WebSocketFrame>): void {
const frame = event.data;
if (!this.frameFilter(frame)) {
return;
}
this.dataGrid.insertChild(new ResourceWebSocketFrameNode(this.request.url(), frame));
}
private frameFilter(frame: SDK.NetworkRequest.WebSocketFrame): boolean {
if (this.filterType && frame.type !== this.filterType) {
return false;
}
return !this.filterRegex || this.filterRegex.test(frame.text);
}
private clearFrames(): void {
// TODO(allada): actially remove frames from request.
_clearFrameOffsets.set(this.request, this.request.frames().length);
this.refresh();
}
private updateFilterSetting(): void {
const text = this.filterTextInput.value();
this.messageFilterSetting.set(text);
this.applyFilter(text);
}
private applyFilter(text: string): void {
const type = (this.filterTypeCombobox.selectedOption() as HTMLOptionElement).value;
this.filterRegex = text ? new RegExp(text, 'i') : null;
this.filterType = type === 'all' ? null : type;
this.refresh();
}
private async onFrameSelected(event: Common.EventTarget.EventTargetEvent<DataGrid.DataGrid.DataGridNode<unknown>>):
Promise<void> {
this.currentSelectedNode = (event.data as ResourceWebSocketFrameNode);
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),
''));
}
private onFrameDeselected(): void {
this.currentSelectedNode = null;
this.splitWidget.setSidebarWidget(this.frameEmptyWidget);
}
refresh(): void {
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)));
}
private sortItems(): void {
this.dataGrid.sortNodes(this.timeComparator, !this.dataGrid.isSortOrderAscending());
}
}
// TODO(crbug.com/1167717): Make this a const enum again
// eslint-disable-next-line rulesdir/const_enum
export enum OpCodes {
ContinuationFrame = 0,
TextFrame = 1,
BinaryFrame = 2,
ConnectionCloseFrame = 8,
PingFrame = 9,
PongFrame = 10,
}
export const opCodeDescriptions: (() => string)[] = (function(): (() => Common.UIString.LocalizedString)[] {
const opCodes = OpCodes;
const map = [];
map[opCodes.ContinuationFrame] = i18nLazyString(UIStrings.continuationFrame);
map[opCodes.TextFrame] = i18nLazyString(UIStrings.textMessage);
map[opCodes.BinaryFrame] = i18nLazyString(UIStrings.binaryMessage);
map[opCodes.ConnectionCloseFrame] = i18nLazyString(UIStrings.connectionCloseMessage);
map[opCodes.PingFrame] = i18nLazyString(UIStrings.pingMessage);
map[opCodes.PongFrame] = i18nLazyString(UIStrings.pongMessage);
return map;
})();
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
// eslint-disable-next-line @typescript-eslint/naming-convention
export const _filterTypes: UI.FilterBar.Item[] = [
{name: 'all', label: i18nLazyString(UIStrings.all), title: undefined},
{name: 'send', label: i18nLazyString(UIStrings.send), title: undefined},
{name: 'receive', label: i18nLazyString(UIStrings.receive), title: undefined},
];
export class ResourceWebSocketFrameNode extends DataGrid.SortableDataGrid.SortableDataGridNode<unknown> {
private readonly url: Platform.DevToolsPath.UrlString;
readonly frame: SDK.NetworkRequest.WebSocketFrame;
private readonly isTextFrame: boolean;
private dataTextInternal: string;
private binaryViewInternal: BinaryResourceView|null;
constructor(url: Platform.DevToolsPath.UrlString, frame: SDK.NetworkRequest.WebSocketFrame) {
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: string = 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 = i18nString(UIStrings.na);
} else if (isTextFrame) {
description = dataText;
} else if (frame.opCode === OpCodes.BinaryFrame) {
length = Platform.NumberUtilities.bytesToString(Platform.StringUtilities.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.dataTextInternal = dataText;
this.binaryViewInternal = null;
}
createCells(element: Element): void {
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);
}
nodeSelfHeight(): number {
return 21;
}
dataText(): string {
return this.dataTextInternal;
}
opCode(): OpCodes {
return this.frame.opCode as OpCodes;
}
binaryView(): BinaryResourceView|null {
if (this.isTextFrame || this.frame.type === SDK.NetworkRequest.WebSocketFrameType.Error) {
return null;
}
if (!this.binaryViewInternal) {
if (this.dataTextInternal.length > 0) {
this.binaryViewInternal = new BinaryResourceView(
this.dataTextInternal, Platform.DevToolsPath.EmptyUrlString, Common.ResourceType.resourceTypes.WebSocket);
}
}
return this.binaryViewInternal;
}
}
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
// eslint-disable-next-line @typescript-eslint/naming-convention
export function ResourceWebSocketFrameNodeTimeComparator(
a: ResourceWebSocketFrameNode, b: ResourceWebSocketFrameNode): number {
return a.frame.time - b.frame.time;
}
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
// eslint-disable-next-line @typescript-eslint/naming-convention
const _clearFrameOffsets = new WeakMap<SDK.NetworkRequest.NetworkRequest, number>();