blob: 3332ca2fd252ccd928b10c2541da7c654ae19626 [file] [log] [blame]
Wolfgang Beyer40530b682022-05-17 13:02:011// Copyright 2022 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import * as Common from '../../../core/common/common.js';
6import * as i18n from '../../../core/i18n/i18n.js';
7import {assertNotNullOrUndefined} from '../../../core/platform/platform.js';
8import * as SDK from '../../../core/sdk/sdk.js';
Wolfgang Beyerd1dd7962022-05-24 15:48:489import * as Buttons from '../../../ui/components/buttons/buttons.js';
Wolfgang Beyer40530b682022-05-17 13:02:0110import * as ComponentHelpers from '../../../ui/components/helpers/helpers.js';
Jack Franklin1f19d522022-07-06 14:00:1811import * as Input from '../../../ui/components/input/input.js';
Wolfgang Beyer40530b682022-05-17 13:02:0112import * as UI from '../../../ui/legacy/legacy.js';
13import * as LitHtml from '../../../ui/lit-html/lit-html.js';
14
15import requestHeadersViewStyles from './RequestHeadersView.css.js';
16
Wolfgang Beyerd1dd7962022-05-24 15:48:4817const RAW_HEADER_CUTOFF = 3000;
Wolfgang Beyer40530b682022-05-17 13:02:0118const {render, html} = LitHtml;
19
20const UIStrings = {
21 /**
22 *@description Text in Request Headers View of the Network panel
23 */
Wolfgang Beyer37fcd282022-06-29 09:34:1724 fromDiskCache: '(from disk cache)',
25 /**
26 *@description Text in Request Headers View of the Network panel
27 */
Wolfgang Beyer40530b682022-05-17 13:02:0128 fromMemoryCache: '(from memory cache)',
29 /**
30 *@description Text in Request Headers View of the Network panel
31 */
Wolfgang Beyer37fcd282022-06-29 09:34:1732 fromPrefetchCache: '(from prefetch cache)',
33 /**
34 *@description Text in Request Headers View of the Network panel
35 */
Wolfgang Beyer40530b682022-05-17 13:02:0136 fromServiceWorker: '(from `service worker`)',
37 /**
38 *@description Text in Request Headers View of the Network panel
39 */
40 fromSignedexchange: '(from signed-exchange)',
41 /**
42 *@description Text in Request Headers View of the Network panel
43 */
Wolfgang Beyer40530b682022-05-17 13:02:0144 fromWebBundle: '(from Web Bundle)',
45 /**
46 *@description Section header for a list of the main aspects of a http request
47 */
48 general: 'General',
49 /**
Wolfgang Beyer235544c2022-05-24 08:07:4550 *@description Label for a checkbox to switch between raw and parsed headers
51 */
52 raw: 'Raw',
53 /**
54 *@description Text in Request Headers View of the Network panel
55 */
Wolfgang Beyer37fcd282022-06-29 09:34:1756 referrerPolicy: 'Referrer Policy',
Wolfgang Beyer235544c2022-05-24 08:07:4557 /**
Wolfgang Beyer37fcd282022-06-29 09:34:1758 *@description Text in Network Log View Columns of the Network panel
Wolfgang Beyer40530b682022-05-17 13:02:0159 */
Wolfgang Beyer37fcd282022-06-29 09:34:1760 remoteAddress: 'Remote Address',
61 /**
62 *@description Text in Request Headers View of the Network panel
63 */
64 requestHeaders: 'Request Headers',
Wolfgang Beyer40530b682022-05-17 13:02:0165 /**
66 *@description The HTTP method of a request
67 */
68 requestMethod: 'Request Method',
69 /**
Wolfgang Beyer37fcd282022-06-29 09:34:1770 *@description The URL of a request
71 */
72 requestUrl: 'Request URL',
73 /**
Wolfgang Beyer235544c2022-05-24 08:07:4574 *@description A context menu item in the Network Log View Columns of the Network panel
75 */
76 responseHeaders: 'Response Headers',
77 /**
Wolfgang Beyerd1dd7962022-05-24 15:48:4878 *@description Text to show more content
79 */
80 showMore: 'Show more',
81 /**
Wolfgang Beyer40530b682022-05-17 13:02:0182 *@description HTTP response code
83 */
84 statusCode: 'Status Code',
Wolfgang Beyer40530b682022-05-17 13:02:0185};
86const str_ = i18n.i18n.registerUIStrings('panels/network/components/RequestHeadersView.ts', UIStrings);
87const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
88
89export class RequestHeadersView extends UI.Widget.VBox {
90 readonly #headersView = new RequestHeadersComponent();
91 readonly #request: SDK.NetworkRequest.NetworkRequest;
92
93 constructor(request: SDK.NetworkRequest.NetworkRequest) {
94 super();
95 this.#request = request;
96 this.contentElement.appendChild(this.#headersView);
97 }
98
99 wasShown(): void {
100 this.#request.addEventListener(SDK.NetworkRequest.Events.RemoteAddressChanged, this.#refreshHeadersView, this);
101 this.#request.addEventListener(SDK.NetworkRequest.Events.FinishedLoading, this.#refreshHeadersView, this);
102 this.#refreshHeadersView();
103 }
104
105 willHide(): void {
106 this.#request.removeEventListener(SDK.NetworkRequest.Events.RemoteAddressChanged, this.#refreshHeadersView, this);
107 this.#request.removeEventListener(SDK.NetworkRequest.Events.FinishedLoading, this.#refreshHeadersView, this);
108 }
109
110 #refreshHeadersView(): void {
111 this.#headersView.data = {
112 request: this.#request,
113 };
114 }
115}
116
117export interface RequestHeadersComponentData {
118 request: SDK.NetworkRequest.NetworkRequest;
119}
120
121export class RequestHeadersComponent extends HTMLElement {
122 static readonly litTagName = LitHtml.literal`devtools-request-headers`;
123 readonly #shadow = this.attachShadow({mode: 'open'});
124 #request?: Readonly<SDK.NetworkRequest.NetworkRequest>;
Wolfgang Beyer235544c2022-05-24 08:07:45125 #showResponseHeadersText = false;
126 #showRequestHeadersText = false;
Wolfgang Beyerd1dd7962022-05-24 15:48:48127 #showResponseHeadersTextFull = false;
128 #showRequestHeadersTextFull = false;
Wolfgang Beyer40530b682022-05-17 13:02:01129
130 set data(data: RequestHeadersComponentData) {
131 this.#request = data.request;
132 this.#render();
133 }
134
135 connectedCallback(): void {
136 this.#shadow.adoptedStyleSheets = [requestHeadersViewStyles];
137 }
138
139 #render(): void {
140 assertNotNullOrUndefined(this.#request);
141
142 // Disabled until https://crbug.com/1079231 is fixed.
143 // clang-format off
144 render(html`
145 ${this.#renderGeneralSection()}
Wolfgang Beyer235544c2022-05-24 08:07:45146 ${this.#renderResponseHeaders()}
147 ${this.#renderRequestHeaders()}
Wolfgang Beyer40530b682022-05-17 13:02:01148 `, this.#shadow, {host: this});
149 // clang-format on
150 }
151
Wolfgang Beyer235544c2022-05-24 08:07:45152 #renderResponseHeaders(): LitHtml.TemplateResult {
153 assertNotNullOrUndefined(this.#request);
154
155 const toggleShowRaw = (): void => {
156 this.#showResponseHeadersText = !this.#showResponseHeadersText;
157 this.#render();
158 };
159
160 // Disabled until https://crbug.com/1079231 is fixed.
161 // clang-format off
162 return html`
163 <${Category.litTagName}
164 @togglerawevent=${toggleShowRaw}
165 .data=${{
166 name: 'responseHeaders',
167 title: i18nString(UIStrings.responseHeaders),
168 headerCount: this.#request.sortedResponseHeaders.length,
169 checked: this.#request.responseHeadersText ? this.#showResponseHeadersText : undefined,
170 } as CategoryData}
171 aria-label=${i18nString(UIStrings.responseHeaders)}
172 >
Wolfgang Beyerd1dd7962022-05-24 15:48:48173 ${this.#showResponseHeadersText ?
174 this.#renderRawHeaders(this.#request.responseHeadersText, true) : html`
Wolfgang Beyer235544c2022-05-24 08:07:45175 ${this.#request.sortedResponseHeaders.map(header => html`
176 <div class="row">
177 <div class="header-name">${header.name}:</div>
178 <div class="header-value">${header.value}</div>
179 </div>
180 `)}
181 `}
182 </${Category.litTagName}>
183 `;
184 }
185
186 #renderRequestHeaders(): LitHtml.TemplateResult {
187 assertNotNullOrUndefined(this.#request);
188
189 const toggleShowRaw = (): void => {
190 this.#showRequestHeadersText = !this.#showRequestHeadersText;
191 this.#render();
192 };
193
194 const requestHeadersText = this.#request.requestHeadersText();
195
196 // Disabled until https://crbug.com/1079231 is fixed.
197 // clang-format off
198 return html`
199 <${Category.litTagName}
200 @togglerawevent=${toggleShowRaw}
201 .data=${{
202 name: 'requestHeaders',
203 title: i18nString(UIStrings.requestHeaders),
204 headerCount: this.#request.requestHeaders().length,
205 checked: requestHeadersText? this.#showRequestHeadersText : undefined,
206 } as CategoryData}
207 aria-label=${i18nString(UIStrings.requestHeaders)}
208 >
Wolfgang Beyerd1dd7962022-05-24 15:48:48209 ${(this.#showRequestHeadersText && requestHeadersText) ?
210 this.#renderRawHeaders(requestHeadersText, false) : html`
Wolfgang Beyer235544c2022-05-24 08:07:45211 ${this.#request.requestHeaders().map(header => html`
212 <div class="row">
213 <div class="header-name">${header.name}:</div>
214 <div class="header-value">${header.value}</div>
215 </div>
216 `)}
217 `}
218 </${Category.litTagName}>
219 `;
220 }
221
Wolfgang Beyerd1dd7962022-05-24 15:48:48222 #renderRawHeaders(rawHeadersText: string, forResponseHeaders: boolean): LitHtml.TemplateResult {
223 const trimmed = rawHeadersText.trim();
224 const showFull = forResponseHeaders ? this.#showResponseHeadersTextFull : this.#showRequestHeadersTextFull;
225 const isShortened = !showFull && trimmed.length > RAW_HEADER_CUTOFF;
226
227 const showMore = ():void => {
228 if (forResponseHeaders) {
229 this.#showResponseHeadersTextFull = true;
230 } else {
231 this.#showRequestHeadersTextFull = true;
232 }
233 this.#render();
234 };
235
236 const onContextMenuOpen = (event: Event): void => {
237 const showFull = forResponseHeaders ? this.#showResponseHeadersTextFull : this.#showRequestHeadersTextFull;
238 if (!showFull) {
239 const contextMenu = new UI.ContextMenu.ContextMenu(event);
240 const section = contextMenu.newSection();
241 section.appendItem(i18nString(UIStrings.showMore), showMore);
242 void contextMenu.show();
243 }
244 };
245
246 const addContextMenuListener = (el: Element):void => {
247 if (isShortened) {
248 el.addEventListener('contextmenu', onContextMenuOpen);
249 }
250 };
251
252 return html`
253 <div class="row raw-headers-row" on-render=${ComponentHelpers.Directives.nodeRenderedCallback(addContextMenuListener)}>
254 <div class="raw-headers">${isShortened ? trimmed.substring(0, RAW_HEADER_CUTOFF) : trimmed}</div>
255 ${isShortened ? html`
256 <${Buttons.Button.Button.litTagName}
257 .size=${Buttons.Button.Size.SMALL}
258 .variant=${Buttons.Button.Variant.SECONDARY}
259 @click=${showMore}
260 >${i18nString(UIStrings.showMore)}</${Buttons.Button.Button.litTagName}>
261 ` : LitHtml.nothing}
262 </div>
263 `;
264 }
265
Wolfgang Beyer40530b682022-05-17 13:02:01266 #renderGeneralSection(): LitHtml.TemplateResult {
267 assertNotNullOrUndefined(this.#request);
268
269 let coloredCircleClassName = 'red-circle';
270 if (this.#request.statusCode < 300 || this.#request.statusCode === 304) {
271 coloredCircleClassName = 'green-circle';
272 } else if (this.#request.statusCode < 400) {
273 coloredCircleClassName = 'yellow-circle';
274 }
275
276 let statusText = this.#request.statusCode + ' ' + this.#request.statusText;
277 let statusTextHasComment = false;
278 if (this.#request.cachedInMemory()) {
279 statusText += ' ' + i18nString(UIStrings.fromMemoryCache);
280 statusTextHasComment = true;
281 } else if (this.#request.fetchedViaServiceWorker) {
282 statusText += ' ' + i18nString(UIStrings.fromServiceWorker);
283 statusTextHasComment = true;
284 } else if (this.#request.redirectSourceSignedExchangeInfoHasNoErrors()) {
285 statusText += ' ' + i18nString(UIStrings.fromSignedexchange);
286 statusTextHasComment = true;
287 } else if (this.#request.webBundleInnerRequestInfo()) {
288 statusText += ' ' + i18nString(UIStrings.fromWebBundle);
289 statusTextHasComment = true;
290 } else if (this.#request.fromPrefetchCache()) {
291 statusText += ' ' + i18nString(UIStrings.fromPrefetchCache);
292 statusTextHasComment = true;
293 } else if (this.#request.cached()) {
294 statusText += ' ' + i18nString(UIStrings.fromDiskCache);
295 statusTextHasComment = true;
296 }
297
298 // Disabled until https://crbug.com/1079231 is fixed.
299 // clang-format off
300 return html`
Wolfgang Beyer235544c2022-05-24 08:07:45301 <${Category.litTagName}
302 .data=${{name: 'general', title: i18nString(UIStrings.general)} as CategoryData}
303 aria-label=${i18nString(UIStrings.general)}
304 >
Wolfgang Beyer40530b682022-05-17 13:02:01305 <div class="row">
306 <div class="header-name">${i18nString(UIStrings.requestUrl)}:</div>
307 <div class="header-value">${this.#request.url()}</div>
308 </div>
309 ${this.#request.statusCode? html`
310 <div class="row">
311 <div class="header-name">${i18nString(UIStrings.requestMethod)}:</div>
312 <div class="header-value">${this.#request.requestMethod}</div>
313 </div>
314 <div class="row">
315 <div class="header-name">${i18nString(UIStrings.statusCode)}:</div>
316 <div class="header-value ${coloredCircleClassName} ${statusTextHasComment ? 'status-with-comment' : ''}">${statusText}</div>
317 </div>
318 ` : ''}
319 ${this.#request.remoteAddress()? html`
320 <div class="row">
321 <div class="header-name">${i18nString(UIStrings.remoteAddress)}:</div>
322 <div class="header-value">${this.#request.remoteAddress()}</div>
323 </div>
324 ` : ''}
325 ${this.#request.referrerPolicy()? html`
326 <div class="row">
327 <div class="header-name">${i18nString(UIStrings.referrerPolicy)}:</div>
328 <div class="header-value">${this.#request.referrerPolicy()}</div>
329 </div>
330 ` : ''}
331 </${Category.litTagName}>
332 `;
333 // clang-format on
334 }
335}
336
Wolfgang Beyer235544c2022-05-24 08:07:45337export class ToggleRawHeadersEvent extends Event {
338 static readonly eventName = 'togglerawevent';
339
340 constructor() {
341 super(ToggleRawHeadersEvent.eventName, {});
342 }
343}
344
Wolfgang Beyer40530b682022-05-17 13:02:01345export interface CategoryData {
346 name: string;
347 title: Common.UIString.LocalizedString;
Wolfgang Beyer235544c2022-05-24 08:07:45348 headerCount?: number;
349 checked?: boolean;
Wolfgang Beyer40530b682022-05-17 13:02:01350}
351
352export class Category extends HTMLElement {
353 static readonly litTagName = LitHtml.literal`devtools-request-headers-category`;
354 readonly #shadow = this.attachShadow({mode: 'open'});
355 #expandedSetting?: Common.Settings.Setting<boolean>;
356 #title: Common.UIString.LocalizedString = Common.UIString.LocalizedEmptyString;
Wolfgang Beyer235544c2022-05-24 08:07:45357 #headerCount?: number = undefined;
358 #checked: boolean|undefined = undefined;
Wolfgang Beyer40530b682022-05-17 13:02:01359
360 connectedCallback(): void {
Jack Franklin1f19d522022-07-06 14:00:18361 this.#shadow.adoptedStyleSheets = [requestHeadersViewStyles, Input.checkboxStyles];
Wolfgang Beyer40530b682022-05-17 13:02:01362 }
363
364 set data(data: CategoryData) {
365 this.#title = data.title;
366 this.#expandedSetting =
367 Common.Settings.Settings.instance().createSetting('request-info-' + data.name + '-category-expanded', true);
Wolfgang Beyer235544c2022-05-24 08:07:45368 this.#headerCount = data.headerCount;
369 this.#checked = data.checked;
Wolfgang Beyer40530b682022-05-17 13:02:01370 this.#render();
371 }
372
Wolfgang Beyer235544c2022-05-24 08:07:45373 #onCheckboxToggle(): void {
374 this.dispatchEvent(new ToggleRawHeadersEvent());
375 }
376
Wolfgang Beyer40530b682022-05-17 13:02:01377 #render(): void {
Wolfgang Beyer235544c2022-05-24 08:07:45378 const isOpen = this.#expandedSetting ? this.#expandedSetting.get() : true;
Wolfgang Beyer40530b682022-05-17 13:02:01379 // Disabled until https://crbug.com/1079231 is fixed.
380 // clang-format off
381 render(html`
Wolfgang Beyer235544c2022-05-24 08:07:45382 <details ?open=${isOpen} @toggle=${this.#onToggle}>
383 <summary class="header" @keydown=${this.#onSummaryKeyDown}>
384 ${this.#title}${this.#headerCount ?
385 html`<span class="header-count"> (${this.#headerCount})</span>` :
386 LitHtml.nothing
387 }
388 ${this.#checked !== undefined ? html`
389 <span class="raw-checkbox-container">
390 <label>
391 <input type="checkbox" .checked=${this.#checked} @change=${this.#onCheckboxToggle} />
392 ${i18nString(UIStrings.raw)}
393 </label>
394 </span>
395 ` : LitHtml.nothing}
396 </summary>
Wolfgang Beyer40530b682022-05-17 13:02:01397 <slot></slot>
398 </details>
399 `, this.#shadow, {host: this});
400 // clang-format on
401 }
402
403 #onSummaryKeyDown(event: KeyboardEvent): void {
404 if (!event.target) {
405 return;
406 }
407 const summaryElement = event.target as HTMLElement;
408 const detailsElement = summaryElement.parentElement as HTMLDetailsElement;
409 if (!detailsElement) {
410 throw new Error('<details> element is not found for a <summary> element');
411 }
412 switch (event.key) {
413 case 'ArrowLeft':
414 detailsElement.open = false;
415 break;
416 case 'ArrowRight':
417 detailsElement.open = true;
418 break;
419 }
420 }
421
422 #onToggle(event: Event): void {
423 this.#expandedSetting?.set((event.target as HTMLDetailsElement).open);
424 }
425}
426
427ComponentHelpers.CustomElements.defineComponent('devtools-request-headers', RequestHeadersComponent);
428ComponentHelpers.CustomElements.defineComponent('devtools-request-headers-category', Category);
429
430declare global {
431 // eslint-disable-next-line @typescript-eslint/no-unused-vars
432 interface HTMLElementTagNameMap {
433 'devtools-request-headers': RequestHeadersComponent;
434 'devtools-request-headers-category': Category;
435 }
436}