debug-server-next
Version:
Dev server for hippy-core.
442 lines (441 loc) • 19.5 kB
JavaScript
// Copyright 2018 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.
/* eslint-disable rulesdir/no_underscored_properties */
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 ProtocolClient from '../../core/protocol_client/protocol_client.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as Bindings from '../../models/bindings/bindings.js';
import * as TextUtils from '../../models/text_utils/text_utils.js';
import * as DataGrid from '../../ui/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 * as LitHtml from '../../ui/lit-html/lit-html.js';
const UIStrings = {
/**
*@description Text for one or a group of functions
*/
method: 'Method',
/**
* @description Text in Protocol Monitor. Title for a table column which shows in which direction
* the particular protocol message was travelling. Values in this column will either be 'sent' or
* 'received'.
*/
direction: 'Direction',
/**
* @description Text in Protocol Monitor of the Protocol Monitor tab. Noun relating to a network request.
*/
request: 'Request',
/**
*@description Title of a cell content in protocol monitor. A Network response refers to the act of acknowledging a
network request. Should not be confused with answer.
*/
response: 'Response',
/**
*@description Text for timestamps of items
*/
timestamp: 'Timestamp',
/**
*@description Text in Protocol Monitor of the Protocol Monitor tab
*/
target: 'Target',
/**
*@description Text to record a series of actions for analysis
*/
record: 'Record',
/**
*@description Text to clear everything
*/
clearAll: 'Clear all',
/**
*@description Text to filter result items
*/
filter: 'Filter',
/**
*@description Text for the documentation of something
*/
documentation: 'Documentation',
/**
*@description Cell text content in Protocol Monitor of the Protocol Monitor tab
*@example {30} PH1
*/
sMs: '{PH1} ms',
/**
*@description Text in Protocol Monitor of the Protocol Monitor tab
*/
noMessageSelected: 'No message selected',
/**
*@description Text in Protocol Monitor for the save button
*/
save: 'Save',
/**
*@description Text in Protocol Monitor to describe the sessions column
*/
session: 'Session',
/**
*@description A placeholder for an input in Protocol Monitor. The input accepts commands that are sent to the backend on Enter. CDP stands for Chrome DevTools Protocol.
*/
sendRawCDPCommand: 'Send a raw `CDP` command',
};
const str_ = i18n.i18n.registerUIStrings('panels/protocol_monitor/ProtocolMonitor.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
const timestampRenderer = (value) => {
return LitHtml.html `${i18nString(UIStrings.sMs, { PH1: String(value) })}`;
};
let protocolMonitorImplInstance;
export class ProtocolMonitorImpl extends UI.Widget.VBox {
_started;
_startTime;
_dataGridRowForId;
_infoWidget;
_dataGridIntegrator;
_filterParser;
_suggestionBuilder;
_textFilterUI;
messages = [];
isRecording = false;
constructor() {
super(true);
this._started = false;
this._startTime = 0;
this._dataGridRowForId = new Map();
const topToolbar = new UI.Toolbar.Toolbar('protocol-monitor-toolbar', this.contentElement);
this.registerRequiredCSS('panels/protocol_monitor/protocolMonitor.css');
this.contentElement.classList.add('protocol-monitor');
const recordButton = new UI.Toolbar.ToolbarToggle(i18nString(UIStrings.record), 'largeicon-start-recording', 'largeicon-stop-recording');
recordButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, () => {
recordButton.setToggled(!recordButton.toggled());
this._setRecording(recordButton.toggled());
});
recordButton.setToggleWithRedColor(true);
topToolbar.appendToolbarItem(recordButton);
recordButton.setToggled(true);
const clearButton = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.clearAll), 'largeicon-clear');
clearButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, () => {
this.messages = [];
this._dataGridIntegrator.update({ ...this._dataGridIntegrator.data(), rows: [] });
});
topToolbar.appendToolbarItem(clearButton);
const saveButton = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.save), 'largeicon-download');
saveButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, () => {
this.saveAsFile();
});
topToolbar.appendToolbarItem(saveButton);
const split = new UI.SplitWidget.SplitWidget(true, true, 'protocol-monitor-panel-split', 250);
split.show(this.contentElement);
this._infoWidget = new InfoWidget();
const dataGridInitialData = {
columns: [
{
id: 'method',
title: i18nString(UIStrings.method),
sortable: false,
widthWeighting: 1,
visible: true,
hideable: false,
},
{
id: 'direction',
title: i18nString(UIStrings.direction),
sortable: true,
widthWeighting: 1,
visible: false,
hideable: true,
},
{
id: 'request',
title: i18nString(UIStrings.request),
sortable: false,
widthWeighting: 1,
visible: true,
hideable: true,
},
{
id: 'response',
title: i18nString(UIStrings.response),
sortable: false,
widthWeighting: 1,
visible: true,
hideable: true,
},
{
id: 'timestamp',
title: i18nString(UIStrings.timestamp),
sortable: true,
widthWeighting: 1,
visible: false,
hideable: true,
},
{
id: 'target',
title: i18nString(UIStrings.target),
sortable: true,
widthWeighting: 1,
visible: false,
hideable: true,
},
{
id: 'session',
title: i18nString(UIStrings.session),
sortable: true,
widthWeighting: 1,
visible: false,
hideable: true,
},
],
rows: [],
contextMenus: {
bodyRow: (menu, columns, row) => {
const methodColumn = DataGrid.DataGridUtils.getRowEntryForColumnId(row, 'method');
const directionColumn = DataGrid.DataGridUtils.getRowEntryForColumnId(row, 'direction');
/**
* You can click the "Filter" item in the context menu to filter the
* protocol monitor entries to those that match the method of the
* current row.
*/
menu.defaultSection().appendItem(i18nString(UIStrings.filter), () => {
const methodColumn = DataGrid.DataGridUtils.getRowEntryForColumnId(row, 'method');
this._textFilterUI.setValue(`method:${methodColumn.value}`, true);
});
/**
* You can click the "Documentation" item in the context menu to be
* taken to the CDP Documentation site entry for the given method.
*/
menu.defaultSection().appendItem(i18nString(UIStrings.documentation), () => {
if (!methodColumn.value) {
return;
}
const [domain, method] = String(methodColumn.value).split('.');
const type = directionColumn.value === 'sent' ? 'method' : 'event';
Host.InspectorFrontendHost.InspectorFrontendHostInstance.openInNewTab(`https://chromedevtools.github.io/devtools-protocol/tot/${domain}#${type}-${method}`);
});
},
},
};
this._dataGridIntegrator =
new DataGrid.DataGridControllerIntegrator.DataGridControllerIntegrator(dataGridInitialData);
this._dataGridIntegrator.dataGrid.addEventListener('cellfocused', (event) => {
const focusedEvent = event;
const focusedRow = focusedEvent.data.row;
const infoWidgetData = {
request: DataGrid.DataGridUtils.getRowEntryForColumnId(focusedRow, 'request'),
response: DataGrid.DataGridUtils.getRowEntryForColumnId(focusedRow, 'response'),
direction: DataGrid.DataGridUtils.getRowEntryForColumnId(focusedRow, 'direction'),
};
this._infoWidget.render(infoWidgetData);
});
this._dataGridIntegrator.dataGrid.addEventListener('newuserfiltertext', (event) => {
const filterTextEvent = event;
this._textFilterUI.setValue(filterTextEvent.data.filterText, /* notify listeners */ true);
});
split.setMainWidget(this._dataGridIntegrator);
split.setSidebarWidget(this._infoWidget);
const keys = ['method', 'request', 'response', 'direction', 'target', 'session'];
this._filterParser = new TextUtils.TextUtils.FilterParser(keys);
this._suggestionBuilder = new UI.FilterSuggestionBuilder.FilterSuggestionBuilder(keys);
this._textFilterUI = new UI.Toolbar.ToolbarInput(i18nString(UIStrings.filter), '', 1, .2, '', this._suggestionBuilder.completions.bind(this._suggestionBuilder), true);
this._textFilterUI.addEventListener(UI.Toolbar.ToolbarInput.Event.TextChanged, event => {
const query = event.data;
const filters = this._filterParser.parse(query);
this._dataGridIntegrator.update({ ...this._dataGridIntegrator.data(), filters });
});
topToolbar.appendToolbarItem(this._textFilterUI);
const onSend = () => {
const value = input.value();
// If input cannot be parsed as json, we assume it's the command name
// for a command without parameters. Otherwise, we expect an object
// with "command" and "parameters" attributes.
let json = null;
try {
json = JSON.parse(value);
}
catch (err) {
}
const command = json ? json.command : value;
const parameters = json ? json.parameters : null;
const test = ProtocolClient.InspectorBackend.test;
// TODO: TS thinks that properties are read-only because
// in TS test is defined as a namespace.
// @ts-ignore
test.sendRawMessage(command, parameters, () => { });
};
const input = new UI.Toolbar.ToolbarInput(i18nString(UIStrings.sendRawCDPCommand), '', 1, .2, '', undefined, false);
input.addEventListener(UI.Toolbar.ToolbarInput.Event.EnterPressed, onSend);
const bottomToolbar = new UI.Toolbar.Toolbar('protocol-monitor-bottom-toolbar', this.contentElement);
bottomToolbar.appendToolbarItem(input);
}
static instance(opts = { forceNew: null }) {
const { forceNew } = opts;
if (!protocolMonitorImplInstance || forceNew) {
protocolMonitorImplInstance = new ProtocolMonitorImpl();
}
return protocolMonitorImplInstance;
}
wasShown() {
if (this._started) {
return;
}
this._started = true;
this._startTime = Date.now();
this._setRecording(true);
}
_setRecording(recording) {
this.isRecording = recording;
const test = ProtocolClient.InspectorBackend.test;
if (recording) {
// TODO: TS thinks that properties are read-only because
// in TS test is defined as a namespace.
// @ts-ignore
test.onMessageSent = this._messageSent.bind(this);
// @ts-ignore
test.onMessageReceived = this._messageReceived.bind(this);
}
else {
// @ts-ignore
test.onMessageSent = null;
// @ts-ignore
test.onMessageReceived = null;
}
}
_targetToString(target) {
if (!target) {
return '';
}
return target.decorateLabel(`${target.name()} ${target === SDK.TargetManager.TargetManager.instance().mainTarget() ? '' : target.id()}`);
}
// eslint-disable
_messageReceived(message, target) {
if (this.isRecording) {
this.messages.push({ ...message, type: 'recv', domain: '-' });
}
if ('id' in message && message.id) {
const existingRow = this._dataGridRowForId.get(message.id);
if (!existingRow) {
return;
}
const allExistingRows = this._dataGridIntegrator.data().rows;
const matchingExistingRowIndex = allExistingRows.findIndex(r => existingRow === r);
const newRowWithUpdate = {
...existingRow,
cells: existingRow.cells.map(cell => {
if (cell.columnId === 'response') {
return {
...cell,
value: JSON.stringify(message.result || message.error),
};
}
return cell;
}),
};
const newRowsArray = [...this._dataGridIntegrator.data().rows];
newRowsArray[matchingExistingRowIndex] = newRowWithUpdate;
// Now we've updated the message, it won't be updated again, so we can delete it from the tracking map.
this._dataGridRowForId.delete(message.id);
this._dataGridIntegrator.update({
...this._dataGridIntegrator.data(),
rows: newRowsArray,
});
return;
}
const sdkTarget = target;
const newRow = {
cells: [
{ columnId: 'method', value: message.method },
{ columnId: 'request', value: '', renderer: DataGrid.DataGridRenderers.codeBlockRenderer },
{
columnId: 'response',
value: JSON.stringify(message.params),
renderer: DataGrid.DataGridRenderers.codeBlockRenderer,
},
{
columnId: 'timestamp',
value: Date.now() - this._startTime,
renderer: timestampRenderer,
},
{ columnId: 'direction', value: 'received' },
{ columnId: 'target', value: this._targetToString(sdkTarget) },
{ columnId: 'session', value: message.sessionId || '' },
],
hidden: false,
};
this._dataGridIntegrator.update({
...this._dataGridIntegrator.data(),
rows: this._dataGridIntegrator.data().rows.concat([newRow]),
});
}
_messageSent(message, target) {
if (this.isRecording) {
this.messages.push({ ...message, type: 'send' });
}
const sdkTarget = target;
const newRow = {
styles: {
'--override-data-grid-row-background-color': 'var(--override-data-grid-sent-message-row-background-color)',
},
cells: [
{ columnId: 'method', value: message.method },
{
columnId: 'request',
value: JSON.stringify(message.params),
renderer: DataGrid.DataGridRenderers.codeBlockRenderer,
},
{ columnId: 'response', value: '(pending)', renderer: DataGrid.DataGridRenderers.codeBlockRenderer },
{
columnId: 'timestamp',
value: Date.now() - this._startTime,
renderer: timestampRenderer,
},
{ columnId: 'direction', value: 'sent' },
{ columnId: 'target', value: this._targetToString(sdkTarget) },
{ columnId: 'session', value: message.sessionId || '' },
],
hidden: false,
};
this._dataGridRowForId.set(message.id, newRow);
this._dataGridIntegrator.update({
...this._dataGridIntegrator.data(),
rows: this._dataGridIntegrator.data().rows.concat([newRow]),
});
}
async saveAsFile() {
const now = new Date();
const fileName = 'ProtocolMonitor-' + Platform.DateUtilities.toISO8601Compact(now) + '.json';
const stream = new Bindings.FileUtils.FileOutputStream();
const accepted = await stream.open(fileName);
if (!accepted) {
return;
}
stream.write(JSON.stringify(this.messages, null, ' '));
stream.close();
}
}
export class InfoWidget extends UI.Widget.VBox {
_tabbedPane;
constructor() {
super();
this._tabbedPane = new UI.TabbedPane.TabbedPane();
this._tabbedPane.appendTab('request', i18nString(UIStrings.request), new UI.Widget.Widget());
this._tabbedPane.appendTab('response', i18nString(UIStrings.response), new UI.Widget.Widget());
this._tabbedPane.show(this.contentElement);
this._tabbedPane.selectTab('response');
this.render(null);
}
render(data) {
if (!data || !data.request || !data.response) {
this._tabbedPane.changeTabView('request', new UI.EmptyWidget.EmptyWidget(i18nString(UIStrings.noMessageSelected)));
this._tabbedPane.changeTabView('response', new UI.EmptyWidget.EmptyWidget(i18nString(UIStrings.noMessageSelected)));
return;
}
const requestEnabled = data && data.direction && data.direction.value === 'sent';
this._tabbedPane.setTabEnabled('request', Boolean(requestEnabled));
if (!requestEnabled) {
this._tabbedPane.selectTab('response');
}
const requestParsed = JSON.parse(String(data.request.value) || 'null');
this._tabbedPane.changeTabView('request', SourceFrame.JSONView.JSONView.createViewSync(requestParsed));
const responseParsed = data.response.value === '(pending)' ? null : JSON.parse(String(data.response.value) || 'null');
this._tabbedPane.changeTabView('response', SourceFrame.JSONView.JSONView.createViewSync(responseParsed));
}
}