chrome-devtools-frontend
Version:
Chrome DevTools UI
205 lines (185 loc) • 8.22 kB
text/typescript
// Copyright (c) 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.
/* eslint-disable rulesdir/no-lit-render-outside-of-view */
import '../../../ui/components/icon_button/icon_button.js';
import * as Common from '../../../core/common/common.js';
import * as i18n from '../../../core/i18n/i18n.js';
import type * as Platform from '../../../core/platform/platform.js';
import type * as SDK from '../../../core/sdk/sdk.js';
import type * as Protocol from '../../../generated/protocol.js';
import type * as Logs from '../../../models/logs/logs.js';
import * as NetworkForward from '../../../panels/network/forward/forward.js';
import * as RenderCoordinator from '../../../ui/components/render_coordinator/render_coordinator.js';
import * as Lit from '../../../ui/lit/lit.js';
import * as VisualLogging from '../../../ui/visual_logging/visual_logging.js';
import requestLinkIconStyles from './requestLinkIcon.css.js';
const {html} = Lit;
const UIStrings = {
/**
* @description Title for a link to show a request in the network panel
* @example {https://example.org/index.html} url
*/
clickToShowRequestInTheNetwork: 'Click to open the network panel and show request for URL: {url}',
/**
* @description Title for an link to show a request that is unavailable because the request couldn't be resolved
*/
requestUnavailableInTheNetwork: 'Request unavailable in the network panel, try reloading the inspected page',
/**
* @description Label for the shortened URL displayed in a link to show a request in the network panel
*/
shortenedURL: 'Shortened URL',
} as const;
const str_ = i18n.i18n.registerUIStrings('ui/components/request_link_icon/RequestLinkIcon.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export interface RequestLinkIconData {
linkToPreflight?: boolean;
request?: SDK.NetworkRequest.NetworkRequest|null;
affectedRequest?: {requestId?: Protocol.Network.RequestId, url?: string};
highlightHeader?: {section: NetworkForward.UIRequestLocation.UIHeaderSection, name: string};
networkTab?: NetworkForward.UIRequestLocation.UIRequestTabs;
requestResolver?: Logs.RequestResolver.RequestResolver;
displayURL?: boolean;
// If displayURL && urlToDisplay !== undefined, uses urlToDisplay for the text of the link.
// If displayURL only, uses filename of the URL.
urlToDisplay?: string;
additionalOnClickAction?: () => void;
revealOverride?: (revealable: unknown, omitFocus?: boolean) => Promise<void>;
}
export const extractShortPath = (path: Platform.DevToolsPath.UrlString): string => {
// 1st regex matches everything after last '/'
// if path ends with '/', 2nd regex returns everything between the last two '/'
return (/[^/]+$/.exec(path) || /[^/]+\/$/.exec(path) || [''])[0];
};
export class RequestLinkIcon extends HTMLElement {
readonly #shadow = this.attachShadow({mode: 'open'});
#linkToPreflight?: boolean;
// The value `null` indicates that the request is not available,
// `undefined` that it is still being resolved.
#request?: SDK.NetworkRequest.NetworkRequest|null;
#highlightHeader?: {section: NetworkForward.UIRequestLocation.UIHeaderSection, name: string};
#requestResolver?: Logs.RequestResolver.RequestResolver;
#displayURL = false;
#urlToDisplay?: string;
#networkTab?: NetworkForward.UIRequestLocation.UIRequestTabs;
#affectedRequest?: {requestId?: Protocol.Network.RequestId, url?: string};
#additionalOnClickAction?: () => void;
#reveal = Common.Revealer.reveal;
set data(data: RequestLinkIconData) {
this.#linkToPreflight = data.linkToPreflight;
this.#request = data.request;
if (data.affectedRequest) {
this.#affectedRequest = {...data.affectedRequest};
}
this.#highlightHeader = data.highlightHeader;
this.#networkTab = data.networkTab;
this.#requestResolver = data.requestResolver;
this.#displayURL = data.displayURL ?? false;
this.#urlToDisplay = data.urlToDisplay;
this.#additionalOnClickAction = data.additionalOnClickAction;
if (data.revealOverride) {
this.#reveal = data.revealOverride;
}
if (!this.#request && typeof data.affectedRequest?.requestId !== 'undefined') {
if (!this.#requestResolver) {
throw new Error('A `RequestResolver` must be provided if an `affectedRequest` is provided.');
}
this.#requestResolver.waitFor(data.affectedRequest.requestId)
.then(request => {
this.#request = request;
return this.#render();
})
.catch(() => {
this.#request = null;
});
}
void this.#render();
}
get data(): RequestLinkIconData {
return {
linkToPreflight: this.#linkToPreflight,
request: this.#request,
affectedRequest: this.#affectedRequest,
highlightHeader: this.#highlightHeader,
networkTab: this.#networkTab,
requestResolver: this.#requestResolver,
displayURL: this.#displayURL,
urlToDisplay: this.#urlToDisplay,
additionalOnClickAction: this.#additionalOnClickAction,
revealOverride: this.#reveal !== Common.Revealer.reveal ? this.#reveal : undefined,
};
}
handleClick(event: MouseEvent): void {
if (event.button !== 0) {
return; // Only handle left-click for now.
}
const linkedRequest = this.#linkToPreflight ? this.#request?.preflightRequest() : this.#request;
if (!linkedRequest) {
return;
}
if (this.#highlightHeader) {
const requestLocation = NetworkForward.UIRequestLocation.UIRequestLocation.header(
linkedRequest, this.#highlightHeader.section, this.#highlightHeader.name);
void this.#reveal(requestLocation);
} else {
const requestLocation = NetworkForward.UIRequestLocation.UIRequestLocation.tab(
linkedRequest, this.#networkTab ?? NetworkForward.UIRequestLocation.UIRequestTabs.HEADERS_COMPONENT);
void this.#reveal(requestLocation);
}
this.#additionalOnClickAction?.();
event.consume();
}
#getTooltip(): Platform.UIString.LocalizedString {
if (this.#request) {
return i18nString(UIStrings.clickToShowRequestInTheNetwork, {url: this.#request.url()});
}
return i18nString(UIStrings.requestUnavailableInTheNetwork);
}
#getUrlForDisplaying(): string|undefined {
if (!this.#displayURL) {
return undefined;
}
if (this.#request) {
return this.#request.url();
}
return this.#affectedRequest?.url;
}
#maybeRenderURL(): Lit.LitTemplate {
const url = this.#getUrlForDisplaying();
if (!url) {
return Lit.nothing;
}
if (this.#urlToDisplay) {
return html`<span title=${url}>${this.#urlToDisplay}</span>`;
}
const filename = extractShortPath(url as Platform.DevToolsPath.UrlString);
return html`<span aria-label=${i18nString(UIStrings.shortenedURL)} title=${url}>${filename}</span>`;
}
async #render(): Promise<void> {
return await RenderCoordinator.write(() => {
// By default we render just the URL for the request link. If we also know
// the concrete network request, or at least its request ID, we surround
// the URL with a button, that opens the request in the Network panel.
let template = this.#maybeRenderURL();
if (this.#request || this.#affectedRequest?.requestId !== undefined) {
// clang-format off
template = html`
<button class=${Lit.Directives.classMap({link: Boolean(this.#request)})}
title=${this.#getTooltip()}
jslog=${VisualLogging.link('request').track({click: true})}
=${this.handleClick}>
<devtools-icon name="arrow-up-down-circle"></devtools-icon>
${template}
</button>`;
// clang-format on
}
Lit.render(html`<style>${requestLinkIconStyles}</style>${template}`, this.#shadow, {host: this});
});
}
}
customElements.define('devtools-request-link-icon', RequestLinkIcon);
declare global {
interface HTMLElementTagNameMap {
'devtools-request-link-icon': RequestLinkIcon;
}
}