UNPKG

chrome-devtools-frontend

Version:
743 lines (643 loc) • 30.4 kB
// Copyright 2022 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 @devtools/no-imperative-dom-api */ /* eslint-disable @devtools/no-lit-render-outside-of-view */ import '../../../ui/legacy/legacy.js'; 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 {assertNotNullOrUndefined} from '../../../core/platform/platform.js'; import * as SDK from '../../../core/sdk/sdk.js'; import * as Protocol from '../../../generated/protocol.js'; import * as Buttons from '../../../ui/components/buttons/buttons.js'; // eslint-disable-next-line @devtools/es-modules-import import emptyWidgetStyles from '../../../ui/legacy/emptyWidget.css.js'; import * as UI from '../../../ui/legacy/legacy.js'; import {Directives, html, render} from '../../../ui/lit/lit.js'; import * as VisualLogging from '../../../ui/visual_logging/visual_logging.js'; import * as PreloadingComponents from './components/components.js'; import {ruleSetTagOrLocationShort} from './components/PreloadingString.js'; import type * as PreloadingHelper from './helper/helper.js'; import preloadingViewStyles from './preloadingView.css.js'; import preloadingViewDropDownStyles from './preloadingViewDropDown.css.js'; const {createRef, ref} = Directives; const UIStrings = { /** * @description DropDown title for filtering preloading attempts by rule set */ filterFilterByRuleSet: 'Filter by rule set', /** * @description DropDown text for filtering preloading attempts by rule set: No filter */ filterAllPreloads: 'All speculative loads', /** * @description Dropdown subtitle for filtering preloading attempts by rule set * when there are no rule sets in the page. */ noRuleSets: 'no rule sets', /** * @description Text in grid: Rule set is valid */ validityValid: 'Valid', /** * @description Text in grid: Rule set must be a valid JSON object */ validityInvalid: 'Invalid', /** * @description Text in grid: Rule set contains invalid rules and they are ignored */ validitySomeRulesInvalid: 'Some rules invalid', /** * @description Text in grid and details: Preloading attempt is not yet triggered. */ statusNotTriggered: 'Not triggered', /** * @description Text in grid and details: Preloading attempt is eligible but pending. */ statusPending: 'Pending', /** * @description Text in grid and details: Preloading is running. */ statusRunning: 'Running', /** * @description Text in grid and details: Preloading finished and the result is ready for the next navigation. */ statusReady: 'Ready', /** * @description Text in grid and details: Ready, then used. */ statusSuccess: 'Success', /** * @description Text in grid and details: Preloading failed. */ statusFailure: 'Failure', /** * @description Text to pretty print a file */ prettyPrint: 'Pretty print', /** * @description Placeholder text if there are no rules to show. https://developer.chrome.com/docs/devtools/application/debugging-speculation-rules */ noRulesDetected: 'No rules detected', /** * @description Placeholder text if there are no rules to show. https://developer.chrome.com/docs/devtools/application/debugging-speculation-rules */ rulesDescription: 'On this page you will see the speculation rules used to prefetch and prerender page navigations.', /** * @description Placeholder text if there are no speculation attempts for prefetching or prerendering urls. https://developer.chrome.com/docs/devtools/application/debugging-speculation-rules */ noPrefetchAttempts: 'No speculation detected', /** * @description Placeholder text if there are no speculation attempts for prefetching or prerendering urls. https://developer.chrome.com/docs/devtools/application/debugging-speculation-rules */ prefetchDescription: 'On this page you will see details on speculative loads.', /** * @description Text for a learn more link */ learnMore: 'Learn more', } as const; const str_ = i18n.i18n.registerUIStrings('panels/application/preloading/PreloadingView.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); const SPECULATION_EXPLANATION_URL = 'https://developer.chrome.com/docs/devtools/application/debugging-speculation-rules' as Platform.DevToolsPath.UrlString; // Used for selector, indicating no filter is specified. const AllRuleSetRootId = Symbol('AllRuleSetRootId'); class PreloadingUIUtils { static status(status: SDK.PreloadingModel.PreloadingStatus): string { // See content/public/browser/preloading.h PreloadingAttemptOutcome. switch (status) { case SDK.PreloadingModel.PreloadingStatus.NOT_TRIGGERED: return i18nString(UIStrings.statusNotTriggered); case SDK.PreloadingModel.PreloadingStatus.PENDING: return i18nString(UIStrings.statusPending); case SDK.PreloadingModel.PreloadingStatus.RUNNING: return i18nString(UIStrings.statusRunning); case SDK.PreloadingModel.PreloadingStatus.READY: return i18nString(UIStrings.statusReady); case SDK.PreloadingModel.PreloadingStatus.SUCCESS: return i18nString(UIStrings.statusSuccess); case SDK.PreloadingModel.PreloadingStatus.FAILURE: return i18nString(UIStrings.statusFailure); // NotSupported is used to handle unreachable case. For example, // there is no code path for // PreloadingTriggeringOutcome::kTriggeredButPending in prefetch, // which is mapped to NotSupported. So, we regard it as an // internal error. case SDK.PreloadingModel.PreloadingStatus.NOT_SUPPORTED: return i18n.i18n.lockedString('Internal error'); } } static preloadsStatusSummary(countsByStatus: Map<SDK.PreloadingModel.PreloadingStatus, number>): string { const LIST = [ SDK.PreloadingModel.PreloadingStatus.NOT_TRIGGERED, SDK.PreloadingModel.PreloadingStatus.PENDING, SDK.PreloadingModel.PreloadingStatus.RUNNING, SDK.PreloadingModel.PreloadingStatus.READY, SDK.PreloadingModel.PreloadingStatus.SUCCESS, SDK.PreloadingModel.PreloadingStatus.FAILURE, ]; return LIST.filter(status => (countsByStatus?.get(status) || 0) > 0) .map(status => (countsByStatus?.get(status) || 0) + ' ' + this.status(status)) .join(', ') .toLocaleLowerCase(); } // Summary of error of rule set shown in grid. static validity({errorType}: Protocol.Preload.RuleSet): string { switch (errorType) { case undefined: return i18nString(UIStrings.validityValid); case Protocol.Preload.RuleSetErrorType.SourceIsNotJsonObject: case Protocol.Preload.RuleSetErrorType.InvalidRulesetLevelTag: return i18nString(UIStrings.validityInvalid); case Protocol.Preload.RuleSetErrorType.InvalidRulesSkipped: return i18nString(UIStrings.validitySomeRulesInvalid); } } // Where a rule set came from, shown in grid. static location(ruleSet: Protocol.Preload.RuleSet): string { if (ruleSet.backendNodeId !== undefined) { return i18n.i18n.lockedString('<script>'); } if (ruleSet.url !== undefined) { return ruleSet.url; } throw new Error('unreachable'); } static processLocalId(id: Protocol.Preload.RuleSetId): string { // RuleSetId is form of '<processId>.<processLocalId>' const index = id.indexOf('.'); return index === -1 ? id : id.slice(index + 1); } } function pageURL(): Platform.DevToolsPath.UrlString { return SDK.TargetManager.TargetManager.instance().scopeTarget()?.inspectedURL() || ('' as Platform.DevToolsPath.UrlString); } export class PreloadingRuleSetView extends UI.Widget.VBox { private model: SDK.PreloadingModel.PreloadingModel; private focusedRuleSetId: Protocol.Preload.RuleSetId|null = null; private readonly warningsContainer: HTMLDivElement; private readonly warningsView = new PreloadingComponents.PreloadingDisabledInfobar.PreloadingDisabledInfobar(); private readonly hsplit: HTMLElement; private readonly ruleSetGrid = new PreloadingComponents.RuleSetGrid.RuleSetGrid(); private readonly ruleSetGridContainerRef = createRef<HTMLDivElement>(); private readonly ruleSetDetailsRef: Directives.Ref<UI.Widget.WidgetElement<PreloadingComponents.RuleSetDetailsView.RuleSetDetailsView>>; private shouldPrettyPrint = Common.Settings.Settings.instance().moduleSetting('auto-pretty-print-minified').get(); constructor(model: SDK.PreloadingModel.PreloadingModel) { super({useShadowDom: true}); this.registerRequiredCSS(emptyWidgetStyles, preloadingViewStyles); this.model = model; SDK.TargetManager.TargetManager.instance().addScopeChangeListener(this.onScopeChange.bind(this)); SDK.TargetManager.TargetManager.instance().addModelListener( SDK.PreloadingModel.PreloadingModel, SDK.PreloadingModel.Events.MODEL_UPDATED, this.render, this, {scoped: true}); SDK.TargetManager.TargetManager.instance().addModelListener( SDK.PreloadingModel.PreloadingModel, SDK.PreloadingModel.Events.WARNINGS_UPDATED, e => { Object.assign(this.warningsView, e.data); }, this, {scoped: true}); // this (VBox) // +- warningsContainer // +- PreloadingWarningsView // +- hsplit // +- leftContainer // +- RuleSetGrid // +- rightContainer // +- RuleSetDetailsView // // - If an row of RuleSetGrid selected, RuleSetDetailsView shows details of it. // - If not, RuleSetDetailsView hides. this.warningsContainer = document.createElement('div'); this.warningsContainer.classList.add('flex-none'); this.contentElement.insertBefore(this.warningsContainer, this.contentElement.firstChild); this.warningsView.show(this.warningsContainer); this.ruleSetGrid.addEventListener( PreloadingComponents.RuleSetGrid.Events.SELECT, this.onRuleSetsGridCellFocused, this); this.ruleSetDetailsRef = createRef<UI.Widget.WidgetElement<PreloadingComponents.RuleSetDetailsView.RuleSetDetailsView>>(); const onPrettyPrintToggle = (): void => { this.shouldPrettyPrint = !this.shouldPrettyPrint; this.updateRuleSetDetails(); }; // clang-format off render( html` <div class="empty-state"> <span class="empty-state-header">${i18nString(UIStrings.noRulesDetected)}</span> <div class="empty-state-description"> <span>${i18nString(UIStrings.rulesDescription)}</span> <x-link class="x-link devtools-link" href=${SPECULATION_EXPLANATION_URL} jslog=${VisualLogging.link().track({click: true, keydown:'Enter|Space'}).context('learn-more')} >${i18nString(UIStrings.learnMore)}</x-link> </div> </div> <devtools-split-view sidebar-position="second"> <div slot="main" ${ref(this.ruleSetGridContainerRef)}> </div> <div slot="sidebar" jslog=${VisualLogging.section('rule-set-details')}> <devtools-widget .widgetConfig=${UI.Widget.widgetConfig(PreloadingComponents.RuleSetDetailsView.RuleSetDetailsView, { ruleSet: this.getRuleSet(), shouldPrettyPrint: this.shouldPrettyPrint, })} ${ref(this.ruleSetDetailsRef)}></devtools-widget> </div> </devtools-split-view> <div class="pretty-print-button" style="border-top: 1px solid var(--sys-color-divider)"> <devtools-button .iconName=${'brackets'} .toggledIconName=${'brackets'} .toggled=${this.shouldPrettyPrint} .toggleType=${Buttons.Button.ToggleType.PRIMARY} .title=${i18nString(UIStrings.prettyPrint)} .variant=${Buttons.Button.Variant.ICON_TOGGLE} .size=${Buttons.Button.Size.REGULAR} @click=${onPrettyPrintToggle} jslog=${VisualLogging.action().track({click: true}).context('preloading-status-panel-pretty-print')}></devtools-button> </div>`, this.contentElement, {host: this}); // clang-format on this.hsplit = this.contentElement.querySelector('devtools-split-view') as HTMLElement; } override wasShown(): void { super.wasShown(); this.warningsView.wasShown(); this.render(); } onScopeChange(): void { const model = SDK.TargetManager.TargetManager.instance().scopeTarget()?.model(SDK.PreloadingModel.PreloadingModel); assertNotNullOrUndefined(model); this.model = model; this.render(); } revealRuleSet(revealInfo: PreloadingHelper.PreloadingForward.RuleSetView): void { this.focusedRuleSetId = revealInfo.ruleSetId; this.render(); } private updateRuleSetDetails(): void { const ruleSet = this.getRuleSet(); const widget = this.ruleSetDetailsRef.value?.getWidget(); if (widget) { widget.shouldPrettyPrint = this.shouldPrettyPrint; widget.ruleSet = ruleSet; } if (ruleSet === null) { this.hsplit.setAttribute('sidebar-visibility', 'hidden'); } else { this.hsplit.removeAttribute('sidebar-visibility'); } } private getRuleSet(): Protocol.Preload.RuleSet|null { const id = this.focusedRuleSetId; return id === null ? null : this.model.getRuleSetById(id); } render(): void { // Update rule sets grid const countsByRuleSetId = this.model.getPreloadCountsByRuleSetId(); const ruleSetRows = this.model.getAllRuleSets().map(({id, value}) => { const countsByStatus = countsByRuleSetId.get(id) || new Map<SDK.PreloadingModel.PreloadingStatus, number>(); return { ruleSet: value, preloadsStatusSummary: PreloadingUIUtils.preloadsStatusSummary(countsByStatus), }; }); this.ruleSetGrid.data = {rows: ruleSetRows, pageURL: pageURL()}; this.contentElement.classList.toggle('empty', ruleSetRows.length === 0); this.updateRuleSetDetails(); const container = this.ruleSetGridContainerRef.value; if (container && this.ruleSetGrid.element.parentElement !== container) { this.ruleSetGrid.show(container); } } private onRuleSetsGridCellFocused(event: Common.EventTarget.EventTargetEvent<Protocol.Preload.RuleSetId>): void { this.focusedRuleSetId = event.data; this.render(); } getInfobarContainerForTest(): HTMLElement { return this.warningsView.contentElement; } getRuleSetGridForTest(): PreloadingComponents.RuleSetGrid.RuleSetGrid { return this.ruleSetGrid; } } export class PreloadingAttemptView extends UI.Widget.VBox { private model: SDK.PreloadingModel.PreloadingModel; // Note that we use id of (representative) preloading attempt while we show pipelines in grid. // This is because `NOT_TRIGGERED` preloading attempts don't have pipeline id and we can use it. private focusedPreloadingAttemptId: SDK.PreloadingModel.PreloadingAttemptId|null = null; private readonly warningsContainer: HTMLDivElement; private readonly warningsView = new PreloadingComponents.PreloadingDisabledInfobar.PreloadingDisabledInfobar(); private readonly preloadingGrid = new PreloadingComponents.PreloadingGrid.PreloadingGrid(); private readonly preloadingDetails = new PreloadingComponents.PreloadingDetailsReportView.PreloadingDetailsReportView(); private readonly ruleSetSelector: PreloadingRuleSetSelector; constructor(model: SDK.PreloadingModel.PreloadingModel) { super({ jslog: `${VisualLogging.pane('preloading-speculations')}`, useShadowDom: true, }); this.registerRequiredCSS(emptyWidgetStyles, preloadingViewStyles); this.model = model; SDK.TargetManager.TargetManager.instance().addScopeChangeListener(this.onScopeChange.bind(this)); SDK.TargetManager.TargetManager.instance().addModelListener( SDK.PreloadingModel.PreloadingModel, SDK.PreloadingModel.Events.MODEL_UPDATED, this.render, this, {scoped: true}); SDK.TargetManager.TargetManager.instance().addModelListener( SDK.PreloadingModel.PreloadingModel, SDK.PreloadingModel.Events.WARNINGS_UPDATED, e => { Object.assign(this.warningsView, e.data); }, this, {scoped: true}); // this (VBox) // +- warningsContainer // +- PreloadingWarningsView // +- VBox // +- toolbar (filtering) // +- hsplit // +- leftContainer // +- PreloadingGrid // +- rightContainer // +- PreloadingDetailsReportView // // - If an row of PreloadingGrid selected, PreloadingDetailsReportView shows details of it. // - If not, PreloadingDetailsReportView shows some messages. this.warningsContainer = document.createElement('div'); this.warningsContainer.classList.add('flex-none'); this.contentElement.insertBefore(this.warningsContainer, this.contentElement.firstChild); this.warningsView.show(this.warningsContainer); const vbox = new UI.Widget.VBox(); const toolbar = vbox.contentElement.createChild('devtools-toolbar', 'preloading-toolbar'); toolbar.setAttribute('jslog', `${VisualLogging.toolbar()}`); this.ruleSetSelector = new PreloadingRuleSetSelector(() => this.render()); toolbar.appendToolbarItem(this.ruleSetSelector.item()); this.preloadingGrid.onSelect = this.onPreloadingGridCellFocused.bind(this); const preloadingGridContainer = document.createElement('div'); preloadingGridContainer.className = 'preloading-grid-widget-container'; preloadingGridContainer.style = 'height: 100%'; this.preloadingGrid.show(preloadingGridContainer, null, true); render( html` <div class="empty-state"> <span class="empty-state-header">${i18nString(UIStrings.noPrefetchAttempts)}</span> <div class="empty-state-description"> <span>${i18nString(UIStrings.prefetchDescription)}</span> <x-link class="x-link devtools-link" href=${SPECULATION_EXPLANATION_URL} jslog=${VisualLogging.link().track({click: true, keydown: 'Enter|Space'}).context('learn-more')} >${i18nString(UIStrings.learnMore)}</x-link> </div> </div> <devtools-split-view sidebar-position="second"> <div slot="main" class="overflow-auto" style="height: 100%"> ${preloadingGridContainer} </div> <div slot="sidebar" class="overflow-auto" style="height: 100%"> ${this.preloadingDetails} </div> </devtools-split-view>`, vbox.contentElement, {host: this}); vbox.show(this.contentElement); } override wasShown(): void { super.wasShown(); this.warningsView.wasShown(); this.render(); } onScopeChange(): void { const model = SDK.TargetManager.TargetManager.instance().scopeTarget()?.model(SDK.PreloadingModel.PreloadingModel); assertNotNullOrUndefined(model); this.model = model; this.render(); } setFilter(filter: PreloadingHelper.PreloadingForward.AttemptViewWithFilter): void { let id: Protocol.Preload.RuleSetId|null = filter.ruleSetId; if (id !== null && this.model.getRuleSetById(id) === undefined) { id = null; } this.ruleSetSelector.select(id); } private updatePreloadingDetails(): void { const id = this.focusedPreloadingAttemptId; const preloadingAttempt = id === null ? null : this.model.getPreloadingAttemptById(id); if (preloadingAttempt === null) { this.preloadingDetails.data = null; } else { const pipeline = this.model.getPipeline(preloadingAttempt); const ruleSets = preloadingAttempt.ruleSetIds.map(id => this.model.getRuleSetById(id)).filter(x => x !== null); this.preloadingDetails.data = { pipeline, ruleSets, pageURL: pageURL(), }; } } render(): void { // Update preloading grid const filteringRuleSetId = this.ruleSetSelector.getSelected(); const rows = this.model.getRepresentativePreloadingAttempts(filteringRuleSetId).map(({id, value}) => { const attempt = value; const pipeline = this.model.getPipeline(attempt); const ruleSets = attempt.ruleSetIds.flatMap(id => { const ruleSet = this.model.getRuleSetById(id); return ruleSet === null ? [] : [ruleSet]; }); return { id, pipeline, ruleSets, }; }); this.preloadingGrid.rows = rows; this.preloadingGrid.pageURL = pageURL(); this.contentElement.classList.toggle('empty', rows.length === 0); this.updatePreloadingDetails(); } private onPreloadingGridCellFocused({rowId}: {rowId: string}): void { this.focusedPreloadingAttemptId = rowId; this.render(); } getRuleSetSelectorToolbarItemForTest(): UI.Toolbar.ToolbarItem { return this.ruleSetSelector.item(); } getPreloadingGridForTest(): PreloadingComponents.PreloadingGrid.PreloadingGrid { return this.preloadingGrid; } getPreloadingDetailsForTest(): PreloadingComponents.PreloadingDetailsReportView.PreloadingDetailsReportView { return this.preloadingDetails; } selectRuleSetOnFilterForTest(id: Protocol.Preload.RuleSetId|null): void { this.ruleSetSelector.select(id); } } export class PreloadingSummaryView extends UI.Widget.VBox { private model: SDK.PreloadingModel.PreloadingModel; private readonly warningsContainer: HTMLDivElement; private readonly warningsView = new PreloadingComponents.PreloadingDisabledInfobar.PreloadingDisabledInfobar(); private readonly usedPreloading = new PreloadingComponents.UsedPreloadingView.UsedPreloadingView(); constructor(model: SDK.PreloadingModel.PreloadingModel) { super({ jslog: `${VisualLogging.pane('speculative-loads')}`, useShadowDom: true, }); this.registerRequiredCSS(emptyWidgetStyles, preloadingViewStyles); this.model = model; SDK.TargetManager.TargetManager.instance().addScopeChangeListener(this.onScopeChange.bind(this)); SDK.TargetManager.TargetManager.instance().addModelListener( SDK.PreloadingModel.PreloadingModel, SDK.PreloadingModel.Events.MODEL_UPDATED, this.render, this, {scoped: true}); SDK.TargetManager.TargetManager.instance().addModelListener( SDK.PreloadingModel.PreloadingModel, SDK.PreloadingModel.Events.WARNINGS_UPDATED, e => { Object.assign(this.warningsView, e.data); }, this, {scoped: true}); this.warningsContainer = document.createElement('div'); this.warningsContainer.classList.add('flex-none'); this.contentElement.insertBefore(this.warningsContainer, this.contentElement.firstChild); this.warningsView.show(this.warningsContainer); const usedPreloadingContainer = new UI.Widget.VBox(); usedPreloadingContainer.contentElement.appendChild(this.usedPreloading); usedPreloadingContainer.show(this.contentElement); } override wasShown(): void { super.wasShown(); this.warningsView.wasShown(); this.render(); } onScopeChange(): void { const model = SDK.TargetManager.TargetManager.instance().scopeTarget()?.model(SDK.PreloadingModel.PreloadingModel); assertNotNullOrUndefined(model); this.model = model; this.render(); } render(): void { this.usedPreloading.data = { pageURL: SDK.TargetManager.TargetManager.instance().scopeTarget()?.inspectedURL() || ('' as Platform.DevToolsPath.UrlString), previousAttempts: this.model.getRepresentativePreloadingAttemptsOfPreviousPage().map(({value}) => value), currentAttempts: this.model.getRepresentativePreloadingAttempts(null).map(({value}) => value), }; } getUsedPreloadingForTest(): PreloadingComponents.UsedPreloadingView.UsedPreloadingView { return this.usedPreloading; } } class PreloadingRuleSetSelector implements UI.Toolbar.Provider, UI.SoftDropDown.Delegate<Protocol.Preload.RuleSetId|typeof AllRuleSetRootId> { private model: SDK.PreloadingModel.PreloadingModel; private readonly onSelectionChanged: () => void = () => {}; private readonly toolbarItem: UI.Toolbar.ToolbarItem; private readonly listModel: UI.ListModel.ListModel<Protocol.Preload.RuleSetId|typeof AllRuleSetRootId>; private readonly dropDown: UI.SoftDropDown.SoftDropDown<Protocol.Preload.RuleSetId|typeof AllRuleSetRootId>; constructor(onSelectionChanged: () => void) { const model = SDK.TargetManager.TargetManager.instance().scopeTarget()?.model(SDK.PreloadingModel.PreloadingModel); assertNotNullOrUndefined(model); this.model = model; SDK.TargetManager.TargetManager.instance().addScopeChangeListener(this.onScopeChange.bind(this)); SDK.TargetManager.TargetManager.instance().addModelListener( SDK.PreloadingModel.PreloadingModel, SDK.PreloadingModel.Events.MODEL_UPDATED, this.onModelUpdated, this, {scoped: true}); this.listModel = new UI.ListModel.ListModel(); this.dropDown = new UI.SoftDropDown.SoftDropDown(this.listModel, this); this.dropDown.setRowHeight(36); this.dropDown.setPlaceholderText(i18nString(UIStrings.filterAllPreloads)); this.toolbarItem = new UI.Toolbar.ToolbarItem(this.dropDown.element); this.toolbarItem.setTitle(i18nString(UIStrings.filterFilterByRuleSet)); this.toolbarItem.element.classList.add('toolbar-has-dropdown'); this.toolbarItem.element.setAttribute( 'jslog', `${VisualLogging.action('filter-by-rule-set').track({click: true})}`); // Initializes `listModel` and `dropDown` using data of the model. this.onModelUpdated(); // Prevents emitting onSelectionChanged on the first call of `this.onModelUpdated()` for initialization. this.onSelectionChanged = onSelectionChanged; } private onScopeChange(): void { const model = SDK.TargetManager.TargetManager.instance().scopeTarget()?.model(SDK.PreloadingModel.PreloadingModel); assertNotNullOrUndefined(model); this.model = model; this.onModelUpdated(); } private onModelUpdated(): void { const ids = this.model.getAllRuleSets().map(({id}) => id); const items = [AllRuleSetRootId, ...ids] as [typeof AllRuleSetRootId, ...Protocol.Preload.RuleSetId[]]; const selected = this.dropDown.getSelectedItem(); // Use `AllRuleSetRootId` by default. For example, `selected` is null or has gone. const newSelected = (selected === null || !items.includes(selected)) ? AllRuleSetRootId : selected; this.listModel.replaceAll(items); this.dropDown.selectItem(newSelected); this.updateWidth(items); } // Updates the width for the DropDown element. private updateWidth(items: Array<Protocol.Preload.RuleSetId|typeof AllRuleSetRootId>): void { // Width set by `UI.SoftDropDown`. const DEFAULT_WIDTH = 315; const urlLengths = items.map(x => this.titleFor(x).length); const maxLength = Math.max(...urlLengths); const width = Math.min(maxLength * 6 + 16, DEFAULT_WIDTH); this.dropDown.setWidth(width); } // AllRuleSetRootId is used within the selector to indicate the root item. When interacting with PreloadingModel, // it should be translated to null. private translateItemIdToRuleSetId(id: Protocol.Preload.RuleSetId|typeof AllRuleSetRootId): Protocol.Preload.RuleSetId |null { if (id === AllRuleSetRootId) { return null; } return id as Protocol.Preload.RuleSetId; } getSelected(): Protocol.Preload.RuleSetId|null { const selectItem = this.dropDown.getSelectedItem(); if (selectItem === null) { return null; } return this.translateItemIdToRuleSetId(selectItem); } select(id: Protocol.Preload.RuleSetId|null): void { this.dropDown.selectItem(id); } // Method for UI.Toolbar.Provider item(): UI.Toolbar.ToolbarItem { return this.toolbarItem; } // Method for UI.SoftDropDown.Delegate<Protocol.Preload.RuleSetId|typeof AllRuleSetRootId> titleFor(id: Protocol.Preload.RuleSetId|typeof AllRuleSetRootId): string { const convertedId = this.translateItemIdToRuleSetId(id); if (convertedId === null) { return i18nString(UIStrings.filterAllPreloads); } const ruleSet = this.model.getRuleSetById(convertedId); if (ruleSet === null) { return i18n.i18n.lockedString('Internal error'); } return ruleSetTagOrLocationShort(ruleSet, pageURL()); } subtitleFor(id: Protocol.Preload.RuleSetId|typeof AllRuleSetRootId): string { const convertedId = this.translateItemIdToRuleSetId(id); const countsByStatus = this.model.getPreloadCountsByRuleSetId().get(convertedId) || new Map<SDK.PreloadingModel.PreloadingStatus, number>(); return PreloadingUIUtils.preloadsStatusSummary(countsByStatus) || `(${i18nString(UIStrings.noRuleSets)})`; } // Method for UI.SoftDropDown.Delegate<Protocol.Preload.RuleSetId|typeof AllRuleSetRootId> createElementForItem(id: Protocol.Preload.RuleSetId|typeof AllRuleSetRootId): Element { const element = document.createElement('div'); const shadowRoot = UI.UIUtils.createShadowRootWithCoreStyles(element, {cssFile: preloadingViewDropDownStyles}); const title = shadowRoot.createChild('div', 'title'); UI.UIUtils.createTextChild(title, Platform.StringUtilities.trimEndWithMaxLength(this.titleFor(id), 100)); const subTitle = shadowRoot.createChild('div', 'subtitle'); UI.UIUtils.createTextChild(subTitle, this.subtitleFor(id)); return element; } // Method for UI.SoftDropDown.Delegate<Protocol.Preload.RuleSetId|typeof AllRuleSetRootId> isItemSelectable(_id: Protocol.Preload.RuleSetId|typeof AllRuleSetRootId): boolean { return true; } // Method for UI.SoftDropDown.Delegate<Protocol.Preload.RuleSetId|typeof AllRuleSetRootId> itemSelected(_id: Protocol.Preload.RuleSetId|typeof AllRuleSetRootId): void { this.onSelectionChanged(); } // Method for UI.SoftDropDown.Delegate<Protocol.Preload.RuleSetId|typeof AllRuleSetRootId> highlightedItemChanged( _from: Protocol.Preload.RuleSetId|typeof AllRuleSetRootId, _to: Protocol.Preload.RuleSetId|typeof AllRuleSetRootId, _fromElement: Element|null, _toElement: Element|null): void { } }