UNPKG

chrome-devtools-frontend

Version:
800 lines (706 loc) 33.9 kB
// Copyright 2017 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-imperative-dom-api */ import '../../ui/components/cards/cards.js'; import * as Common from '../../core/common/common.js'; import * as i18n from '../../core/i18n/i18n.js'; import * as SDK from '../../core/sdk/sdk.js'; import * as Buttons from '../../ui/components/buttons/buttons.js'; import type * as Cards from '../../ui/components/cards/cards.js'; import * as IconButton from '../../ui/components/icon_button/icon_button.js'; import * as UI from '../../ui/legacy/legacy.js'; import * as VisualLogging from '../../ui/visual_logging/visual_logging.js'; import {CalibrationController} from './CalibrationController.js'; import throttlingSettingsTabStyles from './throttlingSettingsTab.css.js'; const UIStrings = { /** *@description Text in Throttling Settings Tab of the Network panel */ networkThrottlingProfiles: 'Network throttling profiles', /** *@description Text of add conditions button in Throttling Settings Tab of the Network panel */ addCustomProfile: 'Add profile', /** *@description A value in milliseconds *@example {3} PH1 */ dms: '{PH1} `ms`', /** *@description Text in Throttling Settings Tab of the Network panel */ profileName: 'Profile Name', /** * @description Label for a textbox that sets the download speed in the Throttling Settings Tab. * Noun, short for 'download speed'. */ download: 'Download', /** * @description Label for a textbox that sets the upload speed in the Throttling Settings Tab. * Noun, short for 'upload speed'. */ upload: 'Upload', /** * @description Label for a textbox that sets the latency in the Throttling Settings Tab. */ latency: 'Latency', /** * @description Label for a textbox that sets the packet loss percentage for real-time networks in the Throttling Settings Tab. */ packetLoss: 'Packet Loss', /** * @description Label for a textbox serving as a unit in the Throttling Settings Tab for the field Packet Loss column. */ percent: 'percent', /** * @description Label for a textbox that sets the maximum packet queue length for real-time networks in the Throttling Settings Tab. */ packetQueueLength: 'Packet Queue Length', /** * @description Label for a checkbox that allows packet reordering in the Throttling Settings Tab. */ packetReordering: 'Packet Reordering', /** * @description Label for a textbox serving as a unit in the Throttling Settings Tab for the field Packet Queue Length column. */ packet: 'packet', /** *@description Text in Throttling Settings Tab of the Network panel */ optional: 'optional', /** *@description Error message for Profile Name input in Throtting pane of the Settings *@example {49} PH1 */ profileNameCharactersLengthMust: 'Profile Name characters length must be between 1 to {PH1} inclusive', /** *@description Error message for Download and Upload inputs in Throttling pane of the Settings *@example {Download} PH1 *@example {0} PH2 *@example {10000000} PH3 */ sMustBeANumberBetweenSkbsToSkbs: '{PH1} must be a number between {PH2} `kbit/s` to {PH3} `kbit/s` inclusive', /** *@description Error message for Latency input in Throttling pane of the Settings *@example {0} PH1 *@example {1000000} PH2 */ latencyMustBeAnIntegerBetweenSms: 'Latency must be an integer between {PH1} `ms` to {PH2} `ms` inclusive', /** *@description Error message for Packet Loss input in Throttling pane of the Settings *@example {0} PH1 *@example {100} PH2 */ packetLossMustBeAnIntegerBetweenSpct: 'Packet Loss must be a number between {PH1} `%` to {PH2} `%` inclusive', /** *@description Error message for Packet Queue Length input in Throttling pane of the Settings */ packetQueueLengthMustBeAnIntegerGreaterOrEqualToZero: 'Packet Queue Length must be greater or equal to 0', /** * @description Text in Throttling Settings Tab of the Network panel, indicating the download or * upload speed that will be applied in kilobits per second. * @example {25} PH1 */ dskbits: '{PH1} `kbit/s`', /** * @description Text in Throttling Settings Tab of the Network panel, indicating the download or * upload speed that will be applied in megabits per second. * @example {25.4} PH1 */ fsmbits: '{PH1} `Mbit/s`', /** * @description Label for the column Packet Reordering to indicate it is enabled in the Throttling Settings Tab. */ on: 'On', /** * @description Label for the column Packet Reordering to indicate it is disabled in the Throttling Settings Tab. */ off: 'Off', /** *@description Text in Throttling Settings Tab of the Settings panel */ cpuThrottlingPresets: 'CPU throttling presets', /** * @description Button text to prompt the user to run the CPU calibration process. */ calibrate: 'Calibrate', /** * @description Button text to prompt the user to re-run the CPU calibration process. */ recalibrate: 'Recalibrate', /** * @description Button text to prompt the user if they wish to continue with the CPU calibration process. */ continue: 'Continue', /** * @description Button text to allow the user to cancel the CPU calibration process. */ cancel: 'Cancel', /** * @description Text to use to indicate that a CPU calibration has not been run yet. */ needsCalibration: 'Needs calibration', /** *@description Text to explain why the user should run the CPU calibration process. */ calibrationCTA: 'To use the CPU throttling presets, run the calibration process to determine the ideal throttling rate for your device.', /** *@description Text to explain what CPU throttling presets are. */ cpuCalibrationDescription: 'These presets throttle your CPU to approximate the performance of typical low or mid-tier mobile devices.', /** *@description Text to explain how the CPU calibration process will work. */ calibrationConfirmationPrompt: 'Calibration will take ~5 seconds, and temporarily navigate away from your current page. Do you wish to continue?', /** *@description Text to explain an issue that may impact the CPU calibration process. */ calibrationWarningHighCPU: 'CPU utilization is too high', /** *@description Text to explain an issue that may impact the CPU calibration process. */ calibrationWarningRunningOnBattery: 'Device is running on battery, please plug in charger for best results', /** *@description Text to explain an issue that may impact the CPU calibration process. */ calibrationWarningLowBattery: 'Device battery is low (<20%), results may be impacted by CPU throttling', /** * @description Text label for a menu item indicating that a specific slowdown multiplier is applied. * @example {2} PH1 */ dSlowdown: '{PH1}× slowdown', } as const; const str_ = i18n.i18n.registerUIStrings('panels/mobile_throttling/ThrottlingSettingsTab.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); /** * This promise resolves after the first compute pressure record is observed. * The object it returns is always up-to-date with the most recent record observed. */ function createComputePressurePromise(): Promise<{state: string}> { const result = {state: ''}; return new Promise(resolve => { // @ts-expect-error typescript/lib version needs to be updated. const observer = new PressureObserver(records => { result.state = records.at(-1).state; resolve(result); }); observer.observe('cpu', { sampleInterval: 1000, }); }); } export class CPUThrottlingCard { element: Cards.Card.Card; private readonly setting: Common.Settings.Setting<SDK.CPUThrottlingManager.CalibratedCPUThrottling>; private computePressurePromise?: ReturnType<typeof createComputePressurePromise>; private controller?: CalibrationController; // UI stuff. private lowTierMobileDeviceEl: HTMLElement; private midTierMobileDeviceEl: HTMLElement; private calibrateEl: HTMLElement; private textEl: HTMLElement; private calibrateButton: Buttons.Button.Button; private cancelButton: Buttons.Button.Button; private progress: UI.ProgressIndicator.ProgressIndicator; private state: 'cta'|'prompting'|'calibrating' = 'cta'; private warnings: string[] = []; constructor() { this.setting = Common.Settings.Settings.instance().createSetting<SDK.CPUThrottlingManager.CalibratedCPUThrottling>( 'calibrated-cpu-throttling', {}, Common.Settings.SettingStorageType.GLOBAL); this.element = document.createElement('devtools-card'); this.element.heading = i18nString(UIStrings.cpuThrottlingPresets); const descriptionEl = this.element.createChild('span'); descriptionEl.textContent = i18nString(UIStrings.cpuCalibrationDescription); this.lowTierMobileDeviceEl = this.element.createChild('div', 'cpu-preset-section'); this.lowTierMobileDeviceEl.append('Low-tier mobile device'); this.lowTierMobileDeviceEl.createChild('div', 'cpu-preset-result'); this.midTierMobileDeviceEl = this.element.createChild('div', 'cpu-preset-section'); this.midTierMobileDeviceEl.append('Mid-tier mobile device'); this.midTierMobileDeviceEl.createChild('div', 'cpu-preset-result'); this.calibrateEl = this.element.createChild('div', 'cpu-preset-section cpu-preset-calibrate'); const buttonContainerEl = this.calibrateEl.createChild('div', 'button-container'); this.calibrateButton = new Buttons.Button.Button(); this.calibrateButton.classList.add('calibrate-button'); this.calibrateButton.data = { variant: Buttons.Button.Variant.PRIMARY, jslogContext: 'throttling.calibrate', }; this.calibrateButton.addEventListener('click', () => this.calibrateButtonClicked()); buttonContainerEl.append(this.calibrateButton); this.cancelButton = new Buttons.Button.Button(); this.cancelButton.classList.add('cancel-button'); this.cancelButton.data = { variant: Buttons.Button.Variant.OUTLINED, jslogContext: 'throttling.calibrate-cancel', }; this.cancelButton.textContent = i18nString(UIStrings.cancel); this.cancelButton.addEventListener('click', () => this.cancelButtonClicked()); buttonContainerEl.append(this.cancelButton); this.textEl = this.calibrateEl.createChild('div', 'text-container'); this.progress = new UI.ProgressIndicator.ProgressIndicator({showStopButton: false}); this.calibrateEl.append(this.progress.element); this.updateState(); } wasShown(): void { this.computePressurePromise = createComputePressurePromise(); this.state = 'cta'; this.updateState(); } willHide(): void { this.computePressurePromise = undefined; if (this.controller) { this.controller.abort(); } } private updateState(): void { if (this.state !== 'calibrating') { this.controller = undefined; } const result = this.setting.get(); const hasCalibrated = result.low || result.mid; this.calibrateButton.style.display = 'none'; this.textEl.style.display = 'none'; this.cancelButton.style.display = 'none'; this.progress.element.style.display = 'none'; if (this.state === 'cta') { this.calibrateButton.style.display = ''; this.calibrateButton.textContent = hasCalibrated ? i18nString(UIStrings.recalibrate) : i18nString(UIStrings.calibrate); if (!hasCalibrated) { this.textEl.style.display = ''; this.textEl.textContent = ''; this.textEl.append(this.createTextWithIcon(i18nString(UIStrings.calibrationCTA), 'info')); } } else if (this.state === 'prompting') { this.calibrateButton.style.display = ''; this.calibrateButton.textContent = i18nString(UIStrings.continue); this.cancelButton.style.display = ''; this.textEl.style.display = ''; this.textEl.textContent = ''; for (const warning of this.warnings) { this.textEl.append(this.createTextWithIcon(warning, 'warning')); } this.textEl.append(this.createTextWithIcon(i18nString(UIStrings.calibrationConfirmationPrompt), 'info')); } else if (this.state === 'calibrating') { this.cancelButton.style.display = ''; this.progress.element.style.display = ''; } const resultToString = (result: number|SDK.CPUThrottlingManager.CalibrationError|undefined): string => { if (result === undefined) { return i18nString(UIStrings.needsCalibration); } if (typeof result === 'string') { return SDK.CPUThrottlingManager.calibrationErrorToString(result); } // Shouldn't happen, but let's not throw an error (.toFixed) if the setting // somehow was saved with a non-number. if (typeof result !== 'number') { return `Invalid: ${result}`; } return i18nString(UIStrings.dSlowdown, {PH1: result.toFixed(1)}); }; const setPresetResult = (element: HTMLElement|null, result: number|SDK.CPUThrottlingManager.CalibrationError|undefined): void => { if (!element) { throw new Error('expected HTMLElement'); } element.textContent = resultToString(result); element.classList.toggle('not-calibrated', result === undefined); }; setPresetResult(this.lowTierMobileDeviceEl.querySelector('.cpu-preset-result'), result.low); setPresetResult(this.midTierMobileDeviceEl.querySelector('.cpu-preset-result'), result.mid); } private createTextWithIcon(text: string, icon: string): HTMLElement { const el = document.createElement('div'); el.classList.add('text-with-icon'); el.append(IconButton.Icon.create(icon)); el.append(text); return el; } private async getCalibrationWarnings(): Promise<string[]> { const warnings = []; if (this.computePressurePromise) { const computePressure = await this.computePressurePromise; if (computePressure.state === 'critical' || computePressure.state === 'serious') { warnings.push(i18nString(UIStrings.calibrationWarningHighCPU)); } } // @ts-expect-error typescript/lib version needs to be updated. const battery = await navigator.getBattery(); if (!battery.charging) { warnings.push(i18nString(UIStrings.calibrationWarningRunningOnBattery)); } else if (battery.level < 0.2) { warnings.push(i18nString(UIStrings.calibrationWarningLowBattery)); } return warnings; } private async calibrateButtonClicked(): Promise<void> { if (this.state === 'cta') { this.warnings = await this.getCalibrationWarnings(); this.state = 'prompting'; this.updateState(); } else if (this.state === 'prompting') { this.state = 'calibrating'; this.updateState(); void this.runCalibration(); } } private cancelButtonClicked(): void { if (this.controller) { this.controller.abort(); } else { this.state = 'cta'; this.updateState(); } } private async runCalibration(): Promise<void> { this.progress.setWorked(0); this.progress.setTotalWork(1); this.controller = new CalibrationController(); try { if (!await this.controller.start()) { console.error('Calibration failed to start'); return; } for await (const result of this.controller.iterator()) { this.progress.setWorked(result.progress); } } catch (e) { console.error(e); } finally { await this.controller.end(); } const result = this.controller.result(); if (result && (result.low || result.mid)) { this.setting.set(result); // Let the user bask in the glory of a 100% progress bar, for a bit. this.progress.setWorked(1); await new Promise(resolve => setTimeout(resolve, 200)); } this.state = 'cta'; this.updateState(); } } export class ThrottlingSettingsTab extends UI.Widget.VBox implements UI.ListWidget.Delegate<SDK.NetworkManager.Conditions> { private readonly list: UI.ListWidget.ListWidget<SDK.NetworkManager.Conditions>; private readonly customSetting: Common.Settings.Setting<SDK.NetworkManager.Conditions[]>; private editor?: UI.ListWidget.Editor<SDK.NetworkManager.Conditions>; private cpuThrottlingCard: CPUThrottlingCard; constructor() { super(true); this.registerRequiredCSS(throttlingSettingsTabStyles); this.element.setAttribute('jslog', `${VisualLogging.pane('throttling-conditions')}`); const settingsContent = this.contentElement.createChild('div', 'settings-card-container-wrapper').createChild('div'); settingsContent.classList.add('settings-card-container', 'throttling-conditions-settings'); this.cpuThrottlingCard = new CPUThrottlingCard(); settingsContent.append(this.cpuThrottlingCard.element); const addButton = new Buttons.Button.Button(); addButton.classList.add('add-conditions-button'); addButton.data = { variant: Buttons.Button.Variant.OUTLINED, iconName: 'plus', jslogContext: 'network.add-conditions', }; addButton.textContent = i18nString(UIStrings.addCustomProfile); addButton.addEventListener('click', () => this.addButtonClicked()); const card = settingsContent.createChild('devtools-card'); card.heading = i18nString(UIStrings.networkThrottlingProfiles); const container = card.createChild('div'); this.list = new UI.ListWidget.ListWidget(this); this.list.element.classList.add('conditions-list'); this.list.registerRequiredCSS(throttlingSettingsTabStyles); this.list.show(container); container.appendChild(addButton); this.customSetting = Common.Settings.Settings.instance().moduleSetting('custom-network-conditions'); this.customSetting.addChangeListener(this.conditionsUpdated, this); } override wasShown(): void { super.wasShown(); this.cpuThrottlingCard.wasShown(); this.conditionsUpdated(); } override willHide(): void { super.willHide(); this.cpuThrottlingCard.willHide(); } private conditionsUpdated(): void { this.list.clear(); const conditions = this.customSetting.get(); for (let i = 0; i < conditions.length; ++i) { this.list.appendItem(conditions[i], true); } this.list.appendSeparator(); } private addButtonClicked(): void { this.list.addNewItem( this.customSetting.get().length, {title: () => '', download: -1, upload: -1, latency: 0, packetLoss: 0, packetReordering: false}); } renderItem(conditions: SDK.NetworkManager.Conditions, _editable: boolean): Element { const element = document.createElement('div'); element.classList.add('conditions-list-item'); const title = element.createChild('div', 'conditions-list-text conditions-list-title'); const titleText = title.createChild('div', 'conditions-list-title-text'); const castedTitle = this.retrieveOptionsTitle(conditions); titleText.textContent = castedTitle; UI.Tooltip.Tooltip.install(titleText, castedTitle); element.createChild('div', 'conditions-list-separator'); element.createChild('div', 'conditions-list-text').textContent = throughputText(conditions.download); element.createChild('div', 'conditions-list-separator'); element.createChild('div', 'conditions-list-text').textContent = throughputText(conditions.upload); element.createChild('div', 'conditions-list-separator'); element.createChild('div', 'conditions-list-text').textContent = i18nString(UIStrings.dms, {PH1: conditions.latency}); element.createChild('div', 'conditions-list-separator'); element.createChild('div', 'conditions-list-text').textContent = percentText(conditions.packetLoss ?? 0); element.createChild('div', 'conditions-list-separator'); element.createChild('div', 'conditions-list-text').textContent = String(conditions.packetQueueLength ?? 0); element.createChild('div', 'conditions-list-separator'); element.createChild('div', 'conditions-list-text').textContent = conditions.packetReordering ? i18nString(UIStrings.on) : i18nString(UIStrings.off); return element; } removeItemRequested(_item: SDK.NetworkManager.Conditions, index: number): void { const list = this.customSetting.get(); list.splice(index, 1); this.customSetting.set(list); } retrieveOptionsTitle(conditions: SDK.NetworkManager.Conditions): string { // The title is usually an i18nLazyString except for custom values that are stored in the local storage in the form of a string. const castedTitle = typeof conditions.title === 'function' ? conditions.title() : conditions.title; return castedTitle; } commitEdit( conditions: SDK.NetworkManager.Conditions, editor: UI.ListWidget.Editor<SDK.NetworkManager.Conditions>, isNew: boolean): void { conditions.title = editor.control('title').value.trim(); const download = editor.control('download').value.trim(); conditions.download = download ? parseInt(download, 10) * (1000 / 8) : -1; const upload = editor.control('upload').value.trim(); conditions.upload = upload ? parseInt(upload, 10) * (1000 / 8) : -1; const latency = editor.control('latency').value.trim(); conditions.latency = latency ? parseInt(latency, 10) : 0; const packetLoss = editor.control('packetLoss').value.trim(); conditions.packetLoss = packetLoss ? parseFloat(packetLoss) : 0; const packetQueueLength = editor.control('packetQueueLength').value.trim(); conditions.packetQueueLength = packetQueueLength ? parseFloat(packetQueueLength) : 0; const packetReordering = (editor.control('packetReordering') as HTMLInputElement).checked; conditions.packetReordering = packetReordering; const list = this.customSetting.get(); if (isNew) { list.push(conditions); } this.customSetting.set(list); } beginEdit(conditions: SDK.NetworkManager.Conditions): UI.ListWidget.Editor<SDK.NetworkManager.Conditions> { const editor = this.createEditor(); editor.control('title').value = this.retrieveOptionsTitle(conditions); editor.control('download').value = conditions.download <= 0 ? '' : String(conditions.download / (1000 / 8)); editor.control('upload').value = conditions.upload <= 0 ? '' : String(conditions.upload / (1000 / 8)); editor.control('latency').value = conditions.latency ? String(conditions.latency) : ''; editor.control('packetLoss').value = conditions.packetLoss ? String(conditions.packetLoss) : ''; editor.control('packetQueueLength').value = conditions.packetQueueLength ? String(conditions.packetQueueLength) : ''; (editor.control('packetReordering') as HTMLInputElement).checked = conditions.packetReordering ?? false; return editor; } private createEditor(): UI.ListWidget.Editor<SDK.NetworkManager.Conditions> { if (this.editor) { return this.editor; } const editor = new UI.ListWidget.Editor<SDK.NetworkManager.Conditions>(); this.editor = editor; const content = editor.contentElement(); const titles = content.createChild('div', 'conditions-edit-row'); const nameLabel = titles.createChild('div', 'conditions-list-text conditions-list-title'); const nameStr = i18nString(UIStrings.profileName); const nameLabelText = nameLabel.createChild('div', 'conditions-list-title-text'); nameLabelText.textContent = nameStr; titles.createChild('div', 'conditions-list-separator conditions-list-separator-invisible'); const downloadLabel = titles.createChild('div', 'conditions-list-text'); const downloadStr = i18nString(UIStrings.download); const downloadLabelText = downloadLabel.createChild('div', 'conditions-list-title-text'); downloadLabelText.textContent = downloadStr; titles.createChild('div', 'conditions-list-separator conditions-list-separator-invisible'); const uploadLabel = titles.createChild('div', 'conditions-list-text'); const uploadLabelText = uploadLabel.createChild('div', 'conditions-list-title-text'); const uploadStr = i18nString(UIStrings.upload); uploadLabelText.textContent = uploadStr; titles.createChild('div', 'conditions-list-separator conditions-list-separator-invisible'); const latencyLabel = titles.createChild('div', 'conditions-list-text'); const latencyStr = i18nString(UIStrings.latency); const latencyLabelText = latencyLabel.createChild('div', 'conditions-list-title-text'); latencyLabelText.textContent = latencyStr; titles.createChild('div', 'conditions-list-separator conditions-list-separator-invisible'); const packetLossLabel = titles.createChild('div', 'conditions-list-text'); const packetLossStr = i18nString(UIStrings.packetLoss); const packetLossLabelText = packetLossLabel.createChild('div', 'conditions-list-title-text'); packetLossLabelText.textContent = packetLossStr; titles.createChild('div', 'conditions-list-separator conditions-list-separator-invisible'); const packetQueueLengthLabel = titles.createChild('div', 'conditions-list-text'); const packetQueueLengthStr = i18nString(UIStrings.packetQueueLength); const packetQueueLengthLabelText = packetQueueLengthLabel.createChild('div', 'conditions-list-title-text'); packetQueueLengthLabelText.textContent = packetQueueLengthStr; titles.createChild('div', 'conditions-list-separator conditions-list-separator-invisible'); const packetReorderingLabel = titles.createChild('div', 'conditions-list-text'); const packetReorderingStr = i18nString(UIStrings.packetReordering); const packetReorderingText = packetReorderingLabel.createChild('div', 'conditions-list-title-text'); packetReorderingText.textContent = packetReorderingStr; const fields = content.createChild('div', 'conditions-edit-row'); const nameInput = editor.createInput('title', 'text', '', titleValidator); UI.ARIAUtils.setLabel(nameInput, nameStr); fields.createChild('div', 'conditions-list-text conditions-list-title').appendChild(nameInput); fields.createChild('div', 'conditions-list-separator conditions-list-separator-invisible'); let cell = fields.createChild('div', 'conditions-list-text'); const downloadInput = editor.createInput('download', 'text', i18n.i18n.lockedString('kbit/s'), throughputValidator); cell.appendChild(downloadInput); UI.ARIAUtils.setLabel(downloadInput, downloadStr); const downloadOptional = cell.createChild('div', 'conditions-edit-optional'); const optionalStr = i18nString(UIStrings.optional); downloadOptional.textContent = optionalStr; UI.ARIAUtils.setDescription(downloadInput, optionalStr); fields.createChild('div', 'conditions-list-separator conditions-list-separator-invisible'); cell = fields.createChild('div', 'conditions-list-text'); const uploadInput = editor.createInput('upload', 'text', i18n.i18n.lockedString('kbit/s'), throughputValidator); UI.ARIAUtils.setLabel(uploadInput, uploadStr); cell.appendChild(uploadInput); const uploadOptional = cell.createChild('div', 'conditions-edit-optional'); uploadOptional.textContent = optionalStr; UI.ARIAUtils.setDescription(uploadInput, optionalStr); fields.createChild('div', 'conditions-list-separator conditions-list-separator-invisible'); cell = fields.createChild('div', 'conditions-list-text'); const latencyInput = editor.createInput('latency', 'text', i18n.i18n.lockedString('ms'), latencyValidator); UI.ARIAUtils.setLabel(latencyInput, latencyStr); cell.appendChild(latencyInput); const latencyOptional = cell.createChild('div', 'conditions-edit-optional'); latencyOptional.textContent = optionalStr; UI.ARIAUtils.setDescription(latencyInput, optionalStr); fields.createChild('div', 'conditions-list-separator conditions-list-separator-invisible'); cell = fields.createChild('div', 'conditions-list-text'); const packetLossInput = editor.createInput('packetLoss', 'text', i18n.i18n.lockedString('percent'), packetLossValidator); UI.ARIAUtils.setLabel(packetLossInput, packetLossStr); cell.appendChild(packetLossInput); const packetLossOptional = cell.createChild('div', 'conditions-edit-optional'); packetLossOptional.textContent = optionalStr; UI.ARIAUtils.setDescription(packetLossInput, optionalStr); fields.createChild('div', 'conditions-list-separator conditions-list-separator-invisible'); cell = fields.createChild('div', 'conditions-list-text'); const packetQueueLengthInput = editor.createInput('packetQueueLength', 'text', i18nString(UIStrings.packet), packetQueueLengthValidator); UI.ARIAUtils.setLabel(packetQueueLengthInput, packetQueueLengthStr); cell.appendChild(packetQueueLengthInput); const packetQueueLengthOptional = cell.createChild('div', 'conditions-edit-optional'); packetQueueLengthOptional.textContent = optionalStr; UI.ARIAUtils.setDescription(packetQueueLengthInput, optionalStr); fields.createChild('div', 'conditions-list-separator conditions-list-separator-invisible'); cell = fields.createChild('div', 'conditions-list-text'); const packetReorderingInput = editor.createInput('packetReordering', 'checkbox', i18nString(UIStrings.percent), packetReorderingValidator); UI.ARIAUtils.setLabel(packetReorderingInput, packetLossStr); cell.appendChild(packetReorderingInput); return editor; function titleValidator(_item: SDK.NetworkManager.Conditions, _index: number, input: UI.ListWidget.EditorControl): UI.ListWidget.ValidatorResult { const maxLength = 49; const value = input.value.trim(); const valid = value.length > 0 && value.length <= maxLength; if (!valid) { const errorMessage = i18nString(UIStrings.profileNameCharactersLengthMust, {PH1: maxLength}); return {valid, errorMessage}; } return {valid, errorMessage: undefined}; } function throughputValidator( _item: SDK.NetworkManager.Conditions, _index: number, input: UI.ListWidget.EditorControl): UI.ListWidget.ValidatorResult { const minThroughput = 0; const maxThroughput = 10000000; const value = input.value.trim(); const parsedValue = Number(value); const throughput = input.getAttribute('aria-label'); const valid = !Number.isNaN(parsedValue) && parsedValue >= minThroughput && parsedValue <= maxThroughput; if (!valid) { const errorMessage = i18nString( UIStrings.sMustBeANumberBetweenSkbsToSkbs, {PH1: String(throughput), PH2: minThroughput, PH3: maxThroughput}); return {valid, errorMessage}; } return {valid, errorMessage: undefined}; } function latencyValidator(_item: SDK.NetworkManager.Conditions, _index: number, input: UI.ListWidget.EditorControl): UI.ListWidget.ValidatorResult { const minLatency = 0; const maxLatency = 1000000; const value = input.value.trim(); const parsedValue = Number(value); const valid = Number.isInteger(parsedValue) && parsedValue >= minLatency && parsedValue <= maxLatency; if (!valid) { const errorMessage = i18nString(UIStrings.latencyMustBeAnIntegerBetweenSms, {PH1: minLatency, PH2: maxLatency}); return {valid, errorMessage}; } return {valid, errorMessage: undefined}; } function packetLossValidator( _item: SDK.NetworkManager.Conditions, _index: number, input: UI.ListWidget.EditorControl): UI.ListWidget.ValidatorResult { const minPacketLoss = 0; const maxPacketLoss = 100; const value = input.value.trim(); const parsedValue = Number(value); const valid = parsedValue >= minPacketLoss && parsedValue <= maxPacketLoss; if (!valid) { const errorMessage = i18nString(UIStrings.packetLossMustBeAnIntegerBetweenSpct, {PH1: minPacketLoss, PH2: maxPacketLoss}); return {valid, errorMessage}; } return {valid, errorMessage: undefined}; } function packetQueueLengthValidator( _item: SDK.NetworkManager.Conditions, _index: number, input: UI.ListWidget.EditorControl): UI.ListWidget.ValidatorResult { const minPacketQueueLength = 0; const value = input.value.trim(); const parsedValue = Number(value); const valid = parsedValue >= minPacketQueueLength; if (!valid) { const errorMessage = i18nString(UIStrings.packetQueueLengthMustBeAnIntegerGreaterOrEqualToZero); return {valid, errorMessage}; } return {valid, errorMessage: undefined}; } function packetReorderingValidator( _item: SDK.NetworkManager.Conditions, _index: number, _input: UI.ListWidget.EditorControl): UI.ListWidget.ValidatorResult { return {valid: true, errorMessage: undefined}; } } } function throughputText(throughput: number): string { if (throughput < 0) { return ''; } const throughputInKbps = throughput / (1000 / 8); if (throughputInKbps < 1000) { return i18nString(UIStrings.dskbits, {PH1: throughputInKbps}); } if (throughputInKbps < 1000 * 10) { const formattedResult = (throughputInKbps / 1000).toFixed(1); return i18nString(UIStrings.fsmbits, {PH1: formattedResult}); } // TODO(petermarshall): Figure out if there is a difference we need to tell i18n about // for these two versions: one with decimal places and one without. return i18nString(UIStrings.fsmbits, {PH1: (throughputInKbps / 1000) | 0}); } function percentText(percent: number): string { if (percent < 0) { return ''; } return String(percent) + '%'; }