chrome-devtools-frontend
Version:
Chrome DevTools UI
363 lines (331 loc) • 16.6 kB
text/typescript
// Copyright 2010 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/* eslint-disable rulesdir/no-imperative-dom-api */
import * as Common from '../../core/common/common.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 type * as NetworkTimeCalculator from '../../models/network_time_calculator/network_time_calculator.js';
import * as NetworkForward from '../../panels/network/forward/forward.js';
import * as IconButton from '../../ui/components/icon_button/icon_button.js';
import * as LegacyWrapper from '../../ui/components/legacy_wrapper/legacy_wrapper.js';
import type * as SourceFrame from '../../ui/legacy/components/source_frame/source_frame.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
import * as NetworkComponents from './components/components.js';
import {EventSourceMessagesView} from './EventSourceMessagesView.js';
import {RequestCookiesView} from './RequestCookiesView.js';
import {RequestInitiatorView} from './RequestInitiatorView.js';
import {RequestPayloadView} from './RequestPayloadView.js';
import {RequestPreviewView} from './RequestPreviewView.js';
import {RequestResponseView} from './RequestResponseView.js';
import {RequestTimingView} from './RequestTimingView.js';
import {ResourceDirectSocketChunkView} from './ResourceDirectSocketChunkView.js';
import {ResourceWebSocketFrameView} from './ResourceWebSocketFrameView.js';
const UIStrings = {
/**
* @description Text for network request headers
*/
headers: 'Headers',
/**
* @description Text for network connection info. In case the request is not made over http.
*/
connectionInfo: 'Connection Info',
/**
* @description Text in Network Item View of the Network panel
*/
payload: 'Payload',
/**
* @description Text in Network Item View of the Network panel
*/
messages: 'Messages',
/**
* @description Text in Network Item View of the Network panel
*/
websocketMessages: 'WebSocket messages',
/**
* @description Text in Network Item View of the Network panel
*/
directsocketMessages: 'DirectSocket messages',
/**
* @description Text in Network Item View of the Network panel
*/
eventstream: 'EventStream',
/**
* @description Text for previewing items
*/
preview: 'Preview',
/**
* @description Text in Network Item View of the Network panel
*/
responsePreview: 'Response preview',
/**
* @description Icon title in Network Item View of the Network panel
*/
signedexchangeError: 'SignedExchange error',
/**
* @description Title of a tab in the Network panel. A Network response refers to the act of acknowledging a
* network request. Should not be confused with answer.
*/
response: 'Response',
/**
* @description Text in Network Item View of the Network panel
*/
rawResponseData: 'Raw response data',
/**
* @description Text for the initiator of something
*/
initiator: 'Initiator',
/**
* @description Tooltip for initiator view in Network panel. An initiator is a piece of code/entity
* in the code that initiated/started the network request, i.e. caused the network request. The 'call
* stack' is the location in the code where the initiation happened.
*/
requestInitiatorCallStack: 'Request initiator call stack',
/**
* @description Title of a tab in Network Item View of the Network panel.
*The tab displays the duration breakdown of a network request.
*/
timing: 'Timing',
/**
* @description Text in Network Item View of the Network panel
*/
requestAndResponseTimeline: 'Request and response timeline',
/**
* @description Tooltip to explain the warning icon of the Cookies panel
*/
thirdPartyPhaseout: 'Cookies blocked due to third-party cookie phaseout.',
/**
* @description Label of a tab in the network panel. Previously known as 'Trust Tokens'.
*/
trustTokens: 'Private state tokens',
/**
* @description Title of the Private State Token tab in the Network panel. Previously known as 'Trust Token tab'.
*/
trustTokenOperationDetails: 'Private State Token operation details',
/**
* @description Text for web cookies
*/
cookies: 'Cookies',
/**
* @description Text in Network Item View of the Network panel
*/
requestAndResponseCookies: 'Request and response cookies',
/**
* @description Tooltip text explaining that DevTools has overridden the response's headers
*/
containsOverriddenHeaders: 'This response contains headers which are overridden by DevTools',
/**
* @description Tooltip text explaining that DevTools has overridden the response
*/
responseIsOverridden: 'This response is overridden by DevTools',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/network/NetworkItemView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
const requestToResponseView = new WeakMap<SDK.NetworkRequest.NetworkRequest, RequestResponseView>();
const requestToPreviewView = new WeakMap<SDK.NetworkRequest.NetworkRequest, RequestPreviewView>();
export class NetworkItemView extends UI.TabbedPane.TabbedPane {
#request: SDK.NetworkRequest.NetworkRequest;
readonly #resourceViewTabSetting: Common.Settings.Setting<NetworkForward.UIRequestLocation.UIRequestTabs>;
readonly #headersViewComponent: NetworkComponents.RequestHeadersView.RequestHeadersView|undefined;
#payloadView: RequestPayloadView|null = null;
readonly #responseView: RequestResponseView|undefined;
#cookiesView: RequestCookiesView|null = null;
#initialTab?: NetworkForward.UIRequestLocation.UIRequestTabs;
readonly #firstTab: NetworkForward.UIRequestLocation.UIRequestTabs;
constructor(
request: SDK.NetworkRequest.NetworkRequest, calculator: NetworkTimeCalculator.NetworkTimeCalculator,
initialTab?: NetworkForward.UIRequestLocation.UIRequestTabs) {
super();
this.#request = request;
this.element.classList.add('network-item-view');
this.headerElement().setAttribute('jslog', `${VisualLogging.toolbar('request-details').track({
keydown: 'ArrowUp|ArrowLeft|ArrowDown|ArrowRight|Enter|Space',
})}`);
if (request.resourceType() === Common.ResourceType.resourceTypes.DirectSocket) {
this.#firstTab = NetworkForward.UIRequestLocation.UIRequestTabs.DIRECT_SOCKET_CONNECTION;
this.appendTab(
NetworkForward.UIRequestLocation.UIRequestTabs.DIRECT_SOCKET_CONNECTION, i18nString(UIStrings.connectionInfo),
new NetworkComponents.DirectSocketConnectionView.DirectSocketConnectionView(request),
i18nString(UIStrings.headers));
} else {
this.#firstTab = NetworkForward.UIRequestLocation.UIRequestTabs.HEADERS_COMPONENT;
this.#headersViewComponent = new NetworkComponents.RequestHeadersView.RequestHeadersView(request);
this.appendTab(
NetworkForward.UIRequestLocation.UIRequestTabs.HEADERS_COMPONENT, i18nString(UIStrings.headers),
LegacyWrapper.LegacyWrapper.legacyWrapper(UI.Widget.VBox, this.#headersViewComponent),
i18nString(UIStrings.headers));
}
this.#resourceViewTabSetting =
Common.Settings.Settings.instance().createSetting('resource-view-tab', this.#firstTab);
if (this.#request.hasOverriddenHeaders()) {
const statusDot = document.createElement('div');
statusDot.className = 'status-dot';
statusDot.title = i18nString(UIStrings.containsOverriddenHeaders);
this.setSuffixElement(NetworkForward.UIRequestLocation.UIRequestTabs.HEADERS_COMPONENT, statusDot);
}
void this.maybeAppendPayloadPanel();
this.addEventListener(UI.TabbedPane.Events.TabSelected, this.tabSelected, this);
if (request.resourceType() === Common.ResourceType.resourceTypes.WebSocket) {
const frameView = new ResourceWebSocketFrameView(request);
this.appendTab(
NetworkForward.UIRequestLocation.UIRequestTabs.WS_FRAMES, i18nString(UIStrings.messages), frameView,
i18nString(UIStrings.websocketMessages));
} else if (request.resourceType() === Common.ResourceType.resourceTypes.DirectSocket) {
this.appendTab(
NetworkForward.UIRequestLocation.UIRequestTabs.DIRECT_SOCKET_CHUNKS, i18nString(UIStrings.messages),
new ResourceDirectSocketChunkView(request), i18nString(UIStrings.directsocketMessages));
} else if (request.mimeType === Platform.MimeType.MimeType.EVENTSTREAM) {
this.appendTab(
NetworkForward.UIRequestLocation.UIRequestTabs.EVENT_SOURCE, i18nString(UIStrings.eventstream),
new EventSourceMessagesView(request));
this.#responseView = requestToResponseView.get(request) ?? new RequestResponseView(request);
requestToResponseView.set(request, this.#responseView);
this.appendTab(
NetworkForward.UIRequestLocation.UIRequestTabs.RESPONSE, i18nString(UIStrings.response), this.#responseView,
i18nString(UIStrings.rawResponseData));
} else {
this.#responseView = requestToResponseView.get(request) ?? new RequestResponseView(request);
requestToResponseView.set(request, this.#responseView);
const previewView = requestToPreviewView.get(request) ?? new RequestPreviewView(request);
requestToPreviewView.set(request, previewView);
this.appendTab(
NetworkForward.UIRequestLocation.UIRequestTabs.PREVIEW, i18nString(UIStrings.preview), previewView,
i18nString(UIStrings.responsePreview));
const signedExchangeInfo = request.signedExchangeInfo();
if (signedExchangeInfo?.errors?.length) {
const icon = new IconButton.Icon.Icon();
icon.name = 'cross-circle-filled';
icon.classList.add('small');
UI.Tooltip.Tooltip.install(icon, i18nString(UIStrings.signedexchangeError));
this.setTabIcon(NetworkForward.UIRequestLocation.UIRequestTabs.PREVIEW, icon);
}
this.appendTab(
NetworkForward.UIRequestLocation.UIRequestTabs.RESPONSE, i18nString(UIStrings.response), this.#responseView,
i18nString(UIStrings.rawResponseData));
if (this.#request.hasOverriddenContent) {
const statusDot = document.createElement('div');
statusDot.className = 'status-dot';
statusDot.title = i18nString(UIStrings.responseIsOverridden);
this.setSuffixElement(NetworkForward.UIRequestLocation.UIRequestTabs.RESPONSE, statusDot);
}
}
this.appendTab(
NetworkForward.UIRequestLocation.UIRequestTabs.INITIATOR, i18nString(UIStrings.initiator),
new RequestInitiatorView(request), i18nString(UIStrings.requestInitiatorCallStack));
this.appendTab(
NetworkForward.UIRequestLocation.UIRequestTabs.TIMING, i18nString(UIStrings.timing),
new RequestTimingView(request, calculator), i18nString(UIStrings.requestAndResponseTimeline));
if (request.trustTokenParams()) {
this.appendTab(
NetworkForward.UIRequestLocation.UIRequestTabs.TRUST_TOKENS, i18nString(UIStrings.trustTokens),
LegacyWrapper.LegacyWrapper.legacyWrapper(
UI.Widget.VBox, new NetworkComponents.RequestTrustTokensView.RequestTrustTokensView(request)),
i18nString(UIStrings.trustTokenOperationDetails));
}
this.#initialTab = initialTab || this.#resourceViewTabSetting.get();
// Selecting tabs should not be handled by the super class.
this.setAutoSelectFirstItemOnShow(false);
}
override wasShown(): void {
super.wasShown();
this.#request.addEventListener(SDK.NetworkRequest.Events.REQUEST_HEADERS_CHANGED, this.requestHeadersChanged, this);
this.#request.addEventListener(
SDK.NetworkRequest.Events.RESPONSE_HEADERS_CHANGED, this.maybeAppendCookiesPanel, this);
this.#request.addEventListener(
SDK.NetworkRequest.Events.TRUST_TOKEN_RESULT_ADDED, this.maybeShowErrorIconInTrustTokenTabHeader, this);
this.maybeAppendCookiesPanel();
this.maybeShowErrorIconInTrustTokenTabHeader();
// Only select the initial tab the first time the view is shown after construction.
// When the view is re-shown (without re-constructing) users or revealers might have changed
// the selected tab in the mean time. Show the previously selected tab in that
// case instead, by simply doing nothing.
if (this.#initialTab) {
this.#selectTab(this.#initialTab);
this.#initialTab = undefined;
}
}
override willHide(): void {
this.#request.removeEventListener(
SDK.NetworkRequest.Events.REQUEST_HEADERS_CHANGED, this.requestHeadersChanged, this);
this.#request.removeEventListener(
SDK.NetworkRequest.Events.RESPONSE_HEADERS_CHANGED, this.maybeAppendCookiesPanel, this);
this.#request.removeEventListener(
SDK.NetworkRequest.Events.TRUST_TOKEN_RESULT_ADDED, this.maybeShowErrorIconInTrustTokenTabHeader, this);
}
private async requestHeadersChanged(): Promise<void> {
this.maybeAppendCookiesPanel();
void this.maybeAppendPayloadPanel();
}
private maybeAppendCookiesPanel(): void {
const cookiesPresent = this.#request.hasRequestCookies() || this.#request.responseCookies.length > 0;
console.assert(cookiesPresent || !this.#cookiesView, 'Cookies were introduced in headers and then removed!');
if (cookiesPresent && !this.#cookiesView) {
this.#cookiesView = new RequestCookiesView(this.#request);
this.appendTab(
NetworkForward.UIRequestLocation.UIRequestTabs.COOKIES, i18nString(UIStrings.cookies), this.#cookiesView,
i18nString(UIStrings.requestAndResponseCookies));
}
if (this.#request.hasThirdPartyCookiePhaseoutIssue()) {
const icon = new IconButton.Icon.Icon();
icon.name = 'warning-filled';
icon.classList.add('small');
icon.title = i18nString(UIStrings.thirdPartyPhaseout);
this.setTrailingTabIcon(NetworkForward.UIRequestLocation.UIRequestTabs.COOKIES, icon);
}
}
private async maybeAppendPayloadPanel(): Promise<void> {
if (this.hasTab('payload')) {
return;
}
if (this.#request.queryParameters || await this.#request.requestFormData()) {
this.#payloadView = new RequestPayloadView(this.#request);
this.appendTab(
NetworkForward.UIRequestLocation.UIRequestTabs.PAYLOAD, i18nString(UIStrings.payload), this.#payloadView,
i18nString(UIStrings.payload), /* userGesture=*/ void 0,
/* isCloseable=*/ void 0, /* isPreviewFeature=*/ void 0, /* index=*/ 1);
}
}
private maybeShowErrorIconInTrustTokenTabHeader(): void {
const trustTokenResult = this.#request.trustTokenOperationDoneEvent();
if (trustTokenResult &&
!NetworkComponents.RequestTrustTokensView.statusConsideredSuccess(trustTokenResult.status)) {
const icon = new IconButton.Icon.Icon();
icon.name = 'cross-circle-filled';
icon.classList.add('small');
this.setTabIcon(NetworkForward.UIRequestLocation.UIRequestTabs.TRUST_TOKENS, icon);
}
}
#selectTab(tabId: NetworkForward.UIRequestLocation.UIRequestTabs): void {
if (!this.selectTab(tabId)) {
// maybeAppendPayloadPanel might cause payload tab to appear asynchronously, so
// it makes sense to retry on the next tick
window.setTimeout(() => {
if (!this.selectTab(tabId)) {
this.selectTab(this.#firstTab);
}
}, 0);
}
}
private tabSelected(event: Common.EventTarget.EventTargetEvent<UI.TabbedPane.EventData>): void {
if (!event.data.isUserGesture) {
return;
}
this.#resourceViewTabSetting.set(event.data.tabId as NetworkForward.UIRequestLocation.UIRequestTabs);
}
request(): SDK.NetworkRequest.NetworkRequest {
return this.#request;
}
async revealResponseBody(position: SourceFrame.SourceFrame.RevealPosition): Promise<void> {
this.#selectTab(NetworkForward.UIRequestLocation.UIRequestTabs.RESPONSE);
await this.#responseView?.revealPosition(position);
}
revealHeader(section: NetworkForward.UIRequestLocation.UIHeaderSection, header: string|undefined): void {
this.#selectTab(NetworkForward.UIRequestLocation.UIRequestTabs.HEADERS_COMPONENT);
this.#headersViewComponent?.revealHeader(section, header);
}
getHeadersViewComponent(): NetworkComponents.RequestHeadersView.RequestHeadersView|undefined {
return this.#headersViewComponent;
}
}