blob: d7fbd0c2aaf28ba52058bee344f08533fabb3fa5 [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';
11import * as UI from '../../../ui/legacy/legacy.js';
12import * as LitHtml from '../../../ui/lit-html/lit-html.js';
13
14import requestHeadersViewStyles from './RequestHeadersView.css.js';
15
Wolfgang Beyerd1dd7962022-05-24 15:48:4816const RAW_HEADER_CUTOFF = 3000;
Wolfgang Beyer40530b682022-05-17 13:02:0117const {render, html} = LitHtml;
18
19const UIStrings = {
20 /**
21 *@description Text in Request Headers View of the Network panel
22 */
23 fromMemoryCache: '(from memory cache)',
24 /**
25 *@description Text in Request Headers View of the Network panel
26 */
27 fromServiceWorker: '(from `service worker`)',
28 /**
29 *@description Text in Request Headers View of the Network panel
30 */
31 fromSignedexchange: '(from signed-exchange)',
32 /**
33 *@description Text in Request Headers View of the Network panel
34 */
35 fromPrefetchCache: '(from prefetch cache)',
36 /**
37 *@description Text in Request Headers View of the Network panel
38 */
39 fromDiskCache: '(from disk cache)',
40 /**
41 *@description Text in Request Headers View of the Network panel
42 */
43 fromWebBundle: '(from Web Bundle)',
44 /**
45 *@description Section header for a list of the main aspects of a http request
46 */
47 general: 'General',
48 /**
Wolfgang Beyer235544c2022-05-24 08:07:4549 *@description Label for a checkbox to switch between raw and parsed headers
50 */
51 raw: 'Raw',
52 /**
53 *@description Text in Request Headers View of the Network panel
54 */
55 requestHeaders: 'Request Headers',
56 /**
Wolfgang Beyer40530b682022-05-17 13:02:0157 *@description The URL of a request
58 */
59 requestUrl: 'Request URL',
60 /**
61 *@description The HTTP method of a request
62 */
63 requestMethod: 'Request Method',
64 /**
Wolfgang Beyer235544c2022-05-24 08:07:4565 *@description A context menu item in the Network Log View Columns of the Network panel
66 */
67 responseHeaders: 'Response Headers',
68 /**
Wolfgang Beyerd1dd7962022-05-24 15:48:4869 *@description Text to show more content
70 */
71 showMore: 'Show more',
72 /**
Wolfgang Beyer40530b682022-05-17 13:02:0173 *@description HTTP response code
74 */
75 statusCode: 'Status Code',
76 /**
77 *@description Text in Network Log View Columns of the Network panel
78 */
79 remoteAddress: 'Remote Address',
80 /**
81 *@description Text in Request Headers View of the Network panel
82 */
83 referrerPolicy: 'Referrer Policy',
84};
85const str_ = i18n.i18n.registerUIStrings('panels/network/components/RequestHeadersView.ts', UIStrings);
86const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
87
88export class RequestHeadersView extends UI.Widget.VBox {
89 readonly #headersView = new RequestHeadersComponent();
90 readonly #request: SDK.NetworkRequest.NetworkRequest;
91
92 constructor(request: SDK.NetworkRequest.NetworkRequest) {
93 super();
94 this.#request = request;
95 this.contentElement.appendChild(this.#headersView);
96 }
97
98 wasShown(): void {
99 this.#request.addEventListener(SDK.NetworkRequest.Events.RemoteAddressChanged, this.#refreshHeadersView, this);
100 this.#request.addEventListener(SDK.NetworkRequest.Events.FinishedLoading, this.#refreshHeadersView, this);
101 this.#refreshHeadersView();
102 }
103
104 willHide(): void {
105 this.#request.removeEventListener(SDK.NetworkRequest.Events.RemoteAddressChanged, this.#refreshHeadersView, this);
106 this.#request.removeEventListener(SDK.NetworkRequest.Events.FinishedLoading, this.#refreshHeadersView, this);
107 }
108
109 #refreshHeadersView(): void {
110 this.#headersView.data = {
111 request: this.#request,
112 };
113 }
114}
115
116export interface RequestHeadersComponentData {
117 request: SDK.NetworkRequest.NetworkRequest;
118}
119
120export class RequestHeadersComponent extends HTMLElement {
121 static readonly litTagName = LitHtml.literal`devtools-request-headers`;
122 readonly #shadow = this.attachShadow({mode: 'open'});
123 #request?: Readonly<SDK.NetworkRequest.NetworkRequest>;
Wolfgang Beyer235544c2022-05-24 08:07:45124 #showResponseHeadersText = false;
125 #showRequestHeadersText = false;
Wolfgang Beyerd1dd7962022-05-24 15:48:48126 #showResponseHeadersTextFull = false;
127 #showRequestHeadersTextFull = false;
Wolfgang Beyer40530b682022-05-17 13:02:01128
129 set data(data: RequestHeadersComponentData) {
130 this.#request = data.request;
131 this.#render();
132 }
133
134 connectedCallback(): void {
135 this.#shadow.adoptedStyleSheets = [requestHeadersViewStyles];
136 }
137
138 #render(): void {
139 assertNotNullOrUndefined(this.#request);
140
141 // Disabled until https://crbug.com/1079231 is fixed.
142 // clang-format off
143 render(html`
144 ${this.#renderGeneralSection()}
Wolfgang Beyer235544c2022-05-24 08:07:45145 ${this.#renderResponseHeaders()}
146 ${this.#renderRequestHeaders()}
Wolfgang Beyer40530b682022-05-17 13:02:01147 `, this.#shadow, {host: this});
148 // clang-format on
149 }
150
Wolfgang Beyer235544c2022-05-24 08:07:45151 #renderResponseHeaders(): LitHtml.TemplateResult {
152 assertNotNullOrUndefined(this.#request);
153
154 const toggleShowRaw = (): void => {
155 this.#showResponseHeadersText = !this.#showResponseHeadersText;
156 this.#render();
157 };
158
159 // Disabled until https://crbug.com/1079231 is fixed.
160 // clang-format off
161 return html`
162 <${Category.litTagName}
163 @togglerawevent=${toggleShowRaw}
164 .data=${{
165 name: 'responseHeaders',
166 title: i18nString(UIStrings.responseHeaders),
167 headerCount: this.#request.sortedResponseHeaders.length,
168 checked: this.#request.responseHeadersText ? this.#showResponseHeadersText : undefined,
169 } as CategoryData}
170 aria-label=${i18nString(UIStrings.responseHeaders)}
171 >
Wolfgang Beyerd1dd7962022-05-24 15:48:48172 ${this.#showResponseHeadersText ?
173 this.#renderRawHeaders(this.#request.responseHeadersText, true) : html`
Wolfgang Beyer235544c2022-05-24 08:07:45174 ${this.#request.sortedResponseHeaders.map(header => html`
175 <div class="row">
176 <div class="header-name">${header.name}:</div>
177 <div class="header-value">${header.value}</div>
178 </div>
179 `)}
180 `}
181 </${Category.litTagName}>
182 `;
183 }
184
185 #renderRequestHeaders(): LitHtml.TemplateResult {
186 assertNotNullOrUndefined(this.#request);
187
188 const toggleShowRaw = (): void => {
189 this.#showRequestHeadersText = !this.#showRequestHeadersText;
190 this.#render();
191 };
192
193 const requestHeadersText = this.#request.requestHeadersText();
194
195 // Disabled until https://crbug.com/1079231 is fixed.
196 // clang-format off
197 return html`
198 <${Category.litTagName}
199 @togglerawevent=${toggleShowRaw}
200 .data=${{
201 name: 'requestHeaders',
202 title: i18nString(UIStrings.requestHeaders),
203 headerCount: this.#request.requestHeaders().length,
204 checked: requestHeadersText? this.#showRequestHeadersText : undefined,
205 } as CategoryData}
206 aria-label=${i18nString(UIStrings.requestHeaders)}
207 >
Wolfgang Beyerd1dd7962022-05-24 15:48:48208 ${(this.#showRequestHeadersText && requestHeadersText) ?
209 this.#renderRawHeaders(requestHeadersText, false) : html`
Wolfgang Beyer235544c2022-05-24 08:07:45210 ${this.#request.requestHeaders().map(header => html`
211 <div class="row">
212 <div class="header-name">${header.name}:</div>
213 <div class="header-value">${header.value}</div>
214 </div>
215 `)}
216 `}
217 </${Category.litTagName}>
218 `;
219 }
220
Wolfgang Beyerd1dd7962022-05-24 15:48:48221 #renderRawHeaders(rawHeadersText: string, forResponseHeaders: boolean): LitHtml.TemplateResult {
222 const trimmed = rawHeadersText.trim();
223 const showFull = forResponseHeaders ? this.#showResponseHeadersTextFull : this.#showRequestHeadersTextFull;
224 const isShortened = !showFull && trimmed.length > RAW_HEADER_CUTOFF;
225
226 const showMore = ():void => {
227 if (forResponseHeaders) {
228 this.#showResponseHeadersTextFull = true;
229 } else {
230 this.#showRequestHeadersTextFull = true;
231 }
232 this.#render();
233 };
234
235 const onContextMenuOpen = (event: Event): void => {
236 const showFull = forResponseHeaders ? this.#showResponseHeadersTextFull : this.#showRequestHeadersTextFull;
237 if (!showFull) {
238 const contextMenu = new UI.ContextMenu.ContextMenu(event);
239 const section = contextMenu.newSection();
240 section.appendItem(i18nString(UIStrings.showMore), showMore);
241 void contextMenu.show();
242 }
243 };
244
245 const addContextMenuListener = (el: Element):void => {
246 if (isShortened) {
247 el.addEventListener('contextmenu', onContextMenuOpen);
248 }
249 };
250
251 return html`
252 <div class="row raw-headers-row" on-render=${ComponentHelpers.Directives.nodeRenderedCallback(addContextMenuListener)}>
253 <div class="raw-headers">${isShortened ? trimmed.substring(0, RAW_HEADER_CUTOFF) : trimmed}</div>
254 ${isShortened ? html`
255 <${Buttons.Button.Button.litTagName}
256 .size=${Buttons.Button.Size.SMALL}
257 .variant=${Buttons.Button.Variant.SECONDARY}
258 @click=${showMore}
259 >${i18nString(UIStrings.showMore)}</${Buttons.Button.Button.litTagName}>
260 ` : LitHtml.nothing}
261 </div>
262 `;
263 }
264
Wolfgang Beyer40530b682022-05-17 13:02:01265 #renderGeneralSection(): LitHtml.TemplateResult {
266 assertNotNullOrUndefined(this.#request);
267
268 let coloredCircleClassName = 'red-circle';
269 if (this.#request.statusCode < 300 || this.#request.statusCode === 304) {
270 coloredCircleClassName = 'green-circle';
271 } else if (this.#request.statusCode < 400) {
272 coloredCircleClassName = 'yellow-circle';
273 }
274
275 let statusText = this.#request.statusCode + ' ' + this.#request.statusText;
276 let statusTextHasComment = false;
277 if (this.#request.cachedInMemory()) {
278 statusText += ' ' + i18nString(UIStrings.fromMemoryCache);
279 statusTextHasComment = true;
280 } else if (this.#request.fetchedViaServiceWorker) {
281 statusText += ' ' + i18nString(UIStrings.fromServiceWorker);
282 statusTextHasComment = true;
283 } else if (this.#request.redirectSourceSignedExchangeInfoHasNoErrors()) {
284 statusText += ' ' + i18nString(UIStrings.fromSignedexchange);
285 statusTextHasComment = true;
286 } else if (this.#request.webBundleInnerRequestInfo()) {
287 statusText += ' ' + i18nString(UIStrings.fromWebBundle);
288 statusTextHasComment = true;
289 } else if (this.#request.fromPrefetchCache()) {
290 statusText += ' ' + i18nString(UIStrings.fromPrefetchCache);
291 statusTextHasComment = true;
292 } else if (this.#request.cached()) {
293 statusText += ' ' + i18nString(UIStrings.fromDiskCache);
294 statusTextHasComment = true;
295 }
296
297 // Disabled until https://crbug.com/1079231 is fixed.
298 // clang-format off
299 return html`
Wolfgang Beyer235544c2022-05-24 08:07:45300 <${Category.litTagName}
301 .data=${{name: 'general', title: i18nString(UIStrings.general)} as CategoryData}
302 aria-label=${i18nString(UIStrings.general)}
303 >
Wolfgang Beyer40530b682022-05-17 13:02:01304 <div class="row">
305 <div class="header-name">${i18nString(UIStrings.requestUrl)}:</div>
306 <div class="header-value">${this.#request.url()}</div>
307 </div>
308 ${this.#request.statusCode? html`
309 <div class="row">
310 <div class="header-name">${i18nString(UIStrings.requestMethod)}:</div>
311 <div class="header-value">${this.#request.requestMethod}</div>
312 </div>
313 <div class="row">
314 <div class="header-name">${i18nString(UIStrings.statusCode)}:</div>
315 <div class="header-value ${coloredCircleClassName} ${statusTextHasComment ? 'status-with-comment' : ''}">${statusText}</div>
316 </div>
317 ` : ''}
318 ${this.#request.remoteAddress()? html`
319 <div class="row">
320 <div class="header-name">${i18nString(UIStrings.remoteAddress)}:</div>
321 <div class="header-value">${this.#request.remoteAddress()}</div>
322 </div>
323 ` : ''}
324 ${this.#request.referrerPolicy()? html`
325 <div class="row">
326 <div class="header-name">${i18nString(UIStrings.referrerPolicy)}:</div>
327 <div class="header-value">${this.#request.referrerPolicy()}</div>
328 </div>
329 ` : ''}
330 </${Category.litTagName}>
331 `;
332 // clang-format on
333 }
334}
335
Wolfgang Beyer235544c2022-05-24 08:07:45336export class ToggleRawHeadersEvent extends Event {
337 static readonly eventName = 'togglerawevent';
338
339 constructor() {
340 super(ToggleRawHeadersEvent.eventName, {});
341 }
342}
343
Wolfgang Beyer40530b682022-05-17 13:02:01344export interface CategoryData {
345 name: string;
346 title: Common.UIString.LocalizedString;
Wolfgang Beyer235544c2022-05-24 08:07:45347 headerCount?: number;
348 checked?: boolean;
Wolfgang Beyer40530b682022-05-17 13:02:01349}
350
351export class Category extends HTMLElement {
352 static readonly litTagName = LitHtml.literal`devtools-request-headers-category`;
353 readonly #shadow = this.attachShadow({mode: 'open'});
354 #expandedSetting?: Common.Settings.Setting<boolean>;
355 #title: Common.UIString.LocalizedString = Common.UIString.LocalizedEmptyString;
Wolfgang Beyer235544c2022-05-24 08:07:45356 #headerCount?: number = undefined;
357 #checked: boolean|undefined = undefined;
Wolfgang Beyer40530b682022-05-17 13:02:01358
359 connectedCallback(): void {
360 this.#shadow.adoptedStyleSheets = [requestHeadersViewStyles];
361 }
362
363 set data(data: CategoryData) {
364 this.#title = data.title;
365 this.#expandedSetting =
366 Common.Settings.Settings.instance().createSetting('request-info-' + data.name + '-category-expanded', true);
Wolfgang Beyer235544c2022-05-24 08:07:45367 this.#headerCount = data.headerCount;
368 this.#checked = data.checked;
Wolfgang Beyer40530b682022-05-17 13:02:01369 this.#render();
370 }
371
Wolfgang Beyer235544c2022-05-24 08:07:45372 #onCheckboxToggle(): void {
373 this.dispatchEvent(new ToggleRawHeadersEvent());
374 }
375
Wolfgang Beyer40530b682022-05-17 13:02:01376 #render(): void {
Wolfgang Beyer235544c2022-05-24 08:07:45377 const isOpen = this.#expandedSetting ? this.#expandedSetting.get() : true;
Wolfgang Beyer40530b682022-05-17 13:02:01378 // Disabled until https://crbug.com/1079231 is fixed.
379 // clang-format off
380 render(html`
Wolfgang Beyer235544c2022-05-24 08:07:45381 <details ?open=${isOpen} @toggle=${this.#onToggle}>
382 <summary class="header" @keydown=${this.#onSummaryKeyDown}>
383 ${this.#title}${this.#headerCount ?
384 html`<span class="header-count"> (${this.#headerCount})</span>` :
385 LitHtml.nothing
386 }
387 ${this.#checked !== undefined ? html`
388 <span class="raw-checkbox-container">
389 <label>
390 <input type="checkbox" .checked=${this.#checked} @change=${this.#onCheckboxToggle} />
391 ${i18nString(UIStrings.raw)}
392 </label>
393 </span>
394 ` : LitHtml.nothing}
395 </summary>
Wolfgang Beyer40530b682022-05-17 13:02:01396 <slot></slot>
397 </details>
398 `, this.#shadow, {host: this});
399 // clang-format on
400 }
401
402 #onSummaryKeyDown(event: KeyboardEvent): void {
403 if (!event.target) {
404 return;
405 }
406 const summaryElement = event.target as HTMLElement;
407 const detailsElement = summaryElement.parentElement as HTMLDetailsElement;
408 if (!detailsElement) {
409 throw new Error('<details> element is not found for a <summary> element');
410 }
411 switch (event.key) {
412 case 'ArrowLeft':
413 detailsElement.open = false;
414 break;
415 case 'ArrowRight':
416 detailsElement.open = true;
417 break;
418 }
419 }
420
421 #onToggle(event: Event): void {
422 this.#expandedSetting?.set((event.target as HTMLDetailsElement).open);
423 }
424}
425
426ComponentHelpers.CustomElements.defineComponent('devtools-request-headers', RequestHeadersComponent);
427ComponentHelpers.CustomElements.defineComponent('devtools-request-headers-category', Category);
428
429declare global {
430 // eslint-disable-next-line @typescript-eslint/no-unused-vars
431 interface HTMLElementTagNameMap {
432 'devtools-request-headers': RequestHeadersComponent;
433 'devtools-request-headers-category': Category;
434 }
435}