UNPKG

chrome-devtools-frontend

Version:
339 lines (307 loc) • 13.7 kB
// Copyright 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. 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 SDK from '../../core/sdk/sdk.js'; import * as Formatter from '../../models/formatter/formatter.js'; import * as Persistence from '../../models/persistence/persistence.js'; import type * as Workspace from '../../models/workspace/workspace.js'; import type * as Diff from '../../third_party/diff/diff.js'; import * as DiffView from '../../ui/components/diff_view/diff_view.js'; import {Directives, html, type TemplateResult} from '../../ui/lit/lit.js'; import * as Snippets from '../snippets/snippets.js'; const {ref, styleMap, ifDefined} = Directives; const UIStrings = { /** *@description Tooltip to explain the resource's overridden status */ requestContentHeadersOverridden: 'Both request content and headers are overridden', /** *@description Tooltip to explain the resource's overridden status */ requestContentOverridden: 'Request content is overridden', /** *@description Tooltip to explain the resource's overridden status */ requestHeadersOverridden: 'Request headers are overridden', /** *@description Tooltip to explain why the request has warning icon */ thirdPartyPhaseout: 'Cookies for this request are blocked either because of Chrome flags or browser configuration. Learn more in the Issues panel.', } as const; const str_ = i18n.i18n.registerUIStrings('panels/utils/utils.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); // These utilities are packaged in a class to allow unittests to stub or spy the implementation. export class PanelUtils { static isFailedNetworkRequest(request: SDK.NetworkRequest.NetworkRequest|null): boolean { if (!request) { return false; } if (request.failed && !request.statusCode) { return true; } if (request.statusCode >= 400) { return true; } const signedExchangeInfo = request.signedExchangeInfo(); if (signedExchangeInfo !== null && Boolean(signedExchangeInfo.errors)) { return true; } if (request.webBundleInfo()?.errorMessage || request.webBundleInnerRequestInfo()?.errorMessage) { return true; } if (request.corsErrorStatus()) { return true; } return false; } static getIconForNetworkRequest(request: SDK.NetworkRequest.NetworkRequest): TemplateResult { let type = request.resourceType(); if (PanelUtils.isFailedNetworkRequest(request)) { let iconName: string; let color: string; // Failed prefetch network requests are displayed as warnings instead of errors. if (request.resourceType() === Common.ResourceType.resourceTypes.Prefetch) { iconName = 'warning-filled'; color = 'var(--icon-warning)'; } else { iconName = 'cross-circle-filled'; color = 'var(--icon-error)'; } // clang-format off return html`<devtools-icon class="icon" name=${iconName} title=${type.title()} style=${styleMap({color})}> </devtools-icon>`; // clang-format on } if (request.hasThirdPartyCookiePhaseoutIssue()) { // clang-format off return html`<devtools-icon class="icon" name="warning-filled" title=${i18nString(UIStrings.thirdPartyPhaseout)} style="color:var(--icon-warning)"> </devtools-icon>`; // clang-format on } const isHeaderOverridden = request.hasOverriddenHeaders(); const isContentOverridden = request.hasOverriddenContent; if (isHeaderOverridden || isContentOverridden) { let title: Common.UIString.LocalizedString; if (isHeaderOverridden && isContentOverridden) { title = i18nString(UIStrings.requestContentHeadersOverridden); } else if (isContentOverridden) { title = i18nString(UIStrings.requestContentOverridden); } else { title = i18nString(UIStrings.requestHeadersOverridden); } // clang-format off return html`<div class="network-override-marker"> <devtools-icon class="icon" name="document" title=${title}></devtools-icon> </div>`; // clang-format on } // Pick icon based on MIME type in the following cases: // - If the MIME type is 'image': some images have request type of 'fetch' or etc. // - If the request type is 'fetch': everything fetched by service worker has request type 'fetch'. // - If the request type is 'other' and MIME type is 'script', e.g. for wasm files const typeFromMime = Common.ResourceType.ResourceType.fromMimeType(request.mimeType); if (typeFromMime !== type && typeFromMime !== Common.ResourceType.resourceTypes.Other) { if (type === Common.ResourceType.resourceTypes.Fetch) { type = typeFromMime; } else if (typeFromMime === Common.ResourceType.resourceTypes.Image) { type = typeFromMime; } else if ( type === Common.ResourceType.resourceTypes.Other && typeFromMime === Common.ResourceType.resourceTypes.Script) { type = typeFromMime; } } if (type === Common.ResourceType.resourceTypes.Image) { return html`<div class="image icon"> <img class="image-network-icon-preview" alt=${request.resourceType().title()} ${ref(e => request.populateImageSource(e as HTMLImageElement))}> </div>`; } // Exclude Manifest here because it has mimeType:application/json but it has its own icon if (type !== Common.ResourceType.resourceTypes.Manifest && Common.ResourceType.ResourceType.simplifyContentType(request.mimeType) === 'application/json') { // clang-format off return html`<devtools-icon class="icon" name="file-json" title=${request.resourceType().title()} style="color:var(--icon-file-script)"> </devtools-icon>`; // clang-format on } // Others const {iconName, color} = PanelUtils.iconDataForResourceType(type); // clang-format off return html`<devtools-icon class="icon" name=${iconName} title=${request.resourceType().title()} style=${styleMap({color})}> </devtools-icon>`; // clang-format on } static iconDataForResourceType(resourceType: Common.ResourceType.ResourceType): {iconName: string, color: string} { if (resourceType.isDocument()) { return {iconName: 'file-document', color: 'var(--icon-file-document)'}; } if (resourceType.isImage()) { return {iconName: 'file-image', color: 'var(--icon-file-image)'}; } if (resourceType.isFont()) { return {iconName: 'file-font', color: 'var(--icon-file-font)'}; } if (resourceType.isScript()) { return {iconName: 'file-script', color: 'var(--icon-file-script)'}; } if (resourceType.isStyleSheet()) { return {iconName: 'file-stylesheet', color: 'var(--icon-file-styles)'}; } if (resourceType.name() === Common.ResourceType.resourceTypes.Manifest.name()) { return {iconName: 'file-manifest', color: 'var(--icon-default)'}; } if (resourceType.name() === Common.ResourceType.resourceTypes.Wasm.name()) { return {iconName: 'file-wasm', color: 'var(--icon-default)'}; } if (resourceType.name() === Common.ResourceType.resourceTypes.WebSocket.name() || resourceType.name() === Common.ResourceType.resourceTypes.DirectSocket.name()) { return {iconName: 'file-websocket', color: 'var(--icon-default)'}; } if (resourceType.name() === Common.ResourceType.resourceTypes.Media.name()) { return {iconName: 'file-media', color: 'var(--icon-file-media)'}; } if (resourceType.isWebbundle()) { return {iconName: 'bundle', color: 'var(--icon-default)'}; } if (resourceType.name() === Common.ResourceType.resourceTypes.Fetch.name() || resourceType.name() === Common.ResourceType.resourceTypes.XHR.name()) { return {iconName: 'file-fetch-xhr', color: 'var(--icon-default)'}; } return {iconName: 'file-generic', color: 'var(--icon-default)'}; } static getIconForSourceFile(uiSourceCode: Workspace.UISourceCode.UISourceCode): TemplateResult { const binding = Persistence.Persistence.PersistenceImpl.instance().binding(uiSourceCode); const networkPersistenceManager = Persistence.NetworkPersistenceManager.NetworkPersistenceManager.instance(); let iconType = 'document'; let hasDotBadge = false; let isDotPurple = false; if (binding) { if (Snippets.ScriptSnippetFileSystem.isSnippetsUISourceCode(binding.fileSystem)) { iconType = 'snippet'; } hasDotBadge = true; isDotPurple = networkPersistenceManager.project() === binding.fileSystem.project(); } else if (networkPersistenceManager.isActiveHeaderOverrides(uiSourceCode)) { hasDotBadge = true; isDotPurple = true; } else if (Snippets.ScriptSnippetFileSystem.isSnippetsUISourceCode(uiSourceCode)) { iconType = 'snippet'; } const title = binding ? Persistence.PersistenceUtils.PersistenceUtils.tooltipForUISourceCode(uiSourceCode) : undefined; // clang-format off return html`<devtools-file-source-icon name=${iconType} title=${ifDefined(title)} .data=${{ contentType: uiSourceCode.contentType().name(), hasDotBadge, isDotPurple, iconType}}> </devtools-file-source-icon>`; // clang-format on } static async formatCSSChangesFromDiff(diff: Diff.Diff.DiffArray): Promise<string> { const indent = ' '; const {originalLines, currentLines, rows} = DiffView.DiffView.buildDiffRows(diff); const originalRuleMaps = await buildStyleRuleMaps(originalLines.join('\n')); const currentRuleMaps = await buildStyleRuleMaps(currentLines.join('\n')); let changes = ''; let recordedOriginalSelector, recordedCurrentSelector; let hasOpenDeclarationBlock = false; for (const {currentLineNumber, originalLineNumber, type} of rows) { if (type !== DiffView.DiffView.RowType.DELETION && type !== DiffView.DiffView.RowType.ADDITION) { continue; } const isDeletion = type === DiffView.DiffView.RowType.DELETION; const lines = isDeletion ? originalLines : currentLines; // Diff line arrays starts at 0, but line numbers start at 1. const lineIndex = isDeletion ? originalLineNumber - 1 : currentLineNumber - 1; const line = lines[lineIndex].trim(); const {declarationIDToStyleRule, styleRuleIDToStyleRule} = isDeletion ? originalRuleMaps : currentRuleMaps; let styleRule; let prefix = ''; if (declarationIDToStyleRule.has(lineIndex)) { styleRule = declarationIDToStyleRule.get(lineIndex) as FormattableStyleRule; const selector = styleRule.selector; // Use the equality of selector strings as a best-effort check for the equality of style rules. if (selector !== recordedOriginalSelector && selector !== recordedCurrentSelector) { prefix += `${selector} {\n`; } prefix += indent; hasOpenDeclarationBlock = true; } else { if (hasOpenDeclarationBlock) { prefix = '}\n\n'; hasOpenDeclarationBlock = false; } if (styleRuleIDToStyleRule.has(lineIndex)) { styleRule = styleRuleIDToStyleRule.get(lineIndex); } } const processedLine = isDeletion ? `/* ${line} */` : line; changes += prefix + processedLine + '\n'; if (isDeletion) { recordedOriginalSelector = styleRule?.selector; } else { recordedCurrentSelector = styleRule?.selector; } } if (changes.length > 0) { changes += '}'; } return changes; } static highlightElement(element: HTMLElement): void { element.scrollIntoViewIfNeeded(); element.animate( [ {offset: 0, backgroundColor: 'rgba(255, 255, 0, 0.2)'}, {offset: 0.1, backgroundColor: 'rgba(255, 255, 0, 0.7)'}, {offset: 1, backgroundColor: 'transparent'}, ], {duration: 2000, easing: 'cubic-bezier(0, 0, 0.2, 1)'}); } } interface FormattableStyleRule { rule: Formatter.FormatterWorkerPool.CSSRule; selector: string; } async function buildStyleRuleMaps(content: string): Promise<{ declarationIDToStyleRule: Map<number, FormattableStyleRule>, styleRuleIDToStyleRule: Map<number, FormattableStyleRule>, }> { const rules = await new Promise<Formatter.FormatterWorkerPool.CSSRule[]>(res => { const rules: Formatter.FormatterWorkerPool.CSSRule[] = []; Formatter.FormatterWorkerPool.formatterWorkerPool().parseCSS(content, (isLastChunk, currentRules) => { rules.push(...currentRules); if (isLastChunk) { res(rules); } }); }); // We use line numbers as unique IDs for rules and declarations const declarationIDToStyleRule = new Map<number, FormattableStyleRule>(); const styleRuleIDToStyleRule = new Map<number, FormattableStyleRule>(); for (const rule of rules) { if ('styleRange' in rule) { const selector = rule.selectorText.split('\n').pop()?.trim(); if (!selector) { continue; } const styleRule = {rule, selector}; styleRuleIDToStyleRule.set(rule.styleRange.startLine, styleRule); for (const property of rule.properties) { declarationIDToStyleRule.set(property.range.startLine, styleRule); } } } return {declarationIDToStyleRule, styleRuleIDToStyleRule}; }