DebugTargetInfo.js (12595B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 "use strict"; 5 6 const { 7 PureComponent, 8 createFactory, 9 } = require("resource://devtools/client/shared/vendor/react.mjs"); 10 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); 11 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs"); 12 const { 13 CONNECTION_TYPES, 14 } = require("resource://devtools/client/shared/remote-debugging/constants.js"); 15 const DESCRIPTOR_TYPES = require("resource://devtools/client/fronts/descriptors/descriptor-types.js"); 16 const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js"); 17 const Localized = createFactory(FluentReact.Localized); 18 19 /** 20 * This is header that should be displayed on top of the toolbox when using 21 * about:devtools-toolbox. 22 */ 23 class DebugTargetInfo extends PureComponent { 24 static get propTypes() { 25 return { 26 alwaysOnTop: PropTypes.bool.isRequired, 27 focusedState: PropTypes.bool, 28 toggleAlwaysOnTop: PropTypes.func.isRequired, 29 debugTargetData: PropTypes.shape({ 30 connectionType: PropTypes.oneOf(Object.values(CONNECTION_TYPES)) 31 .isRequired, 32 runtimeInfo: PropTypes.shape({ 33 deviceName: PropTypes.string, 34 icon: PropTypes.string.isRequired, 35 name: PropTypes.string.isRequired, 36 version: PropTypes.string.isRequired, 37 }).isRequired, 38 descriptorType: PropTypes.oneOf(Object.values(DESCRIPTOR_TYPES)) 39 .isRequired, 40 descriptorName: PropTypes.string.isRequired, 41 }).isRequired, 42 L10N: PropTypes.object.isRequired, 43 toolbox: PropTypes.object.isRequired, 44 }; 45 } 46 47 constructor(props) { 48 super(props); 49 50 this.state = { urlValue: props.toolbox.target.url }; 51 52 this.onChange = this.onChange.bind(this); 53 this.onFocus = this.onFocus.bind(this); 54 this.onSubmit = this.onSubmit.bind(this); 55 } 56 57 componentDidMount() { 58 this.updateTitle(); 59 } 60 61 updateTitle() { 62 const { L10N, debugTargetData, toolbox } = this.props; 63 const title = toolbox.target.name; 64 const descriptorTypeStr = L10N.getStr( 65 this.getAssetsForDebugDescriptorType().l10nId 66 ); 67 68 const { connectionType } = debugTargetData; 69 if (connectionType === CONNECTION_TYPES.THIS_FIREFOX) { 70 toolbox.doc.title = L10N.getFormatStr( 71 "toolbox.debugTargetInfo.tabTitleLocal", 72 descriptorTypeStr, 73 title 74 ); 75 } else { 76 const connectionTypeStr = L10N.getStr( 77 this.getAssetsForConnectionType().l10nId 78 ); 79 toolbox.doc.title = L10N.getFormatStr( 80 "toolbox.debugTargetInfo.tabTitleRemote", 81 connectionTypeStr, 82 descriptorTypeStr, 83 title 84 ); 85 } 86 } 87 88 getRuntimeText() { 89 const { debugTargetData, L10N } = this.props; 90 const { name, version } = debugTargetData.runtimeInfo; 91 const { connectionType } = debugTargetData; 92 const brandShorterName = L10N.getStr("brandShorterName"); 93 94 return connectionType === CONNECTION_TYPES.THIS_FIREFOX 95 ? L10N.getFormatStr( 96 "toolbox.debugTargetInfo.runtimeLabel.thisRuntime", 97 brandShorterName, 98 version 99 ) 100 : L10N.getFormatStr( 101 "toolbox.debugTargetInfo.runtimeLabel", 102 name, 103 version 104 ); 105 } 106 107 getAssetsForConnectionType() { 108 const { connectionType } = this.props.debugTargetData; 109 110 switch (connectionType) { 111 case CONNECTION_TYPES.USB: 112 return { 113 image: "chrome://devtools/skin/images/aboutdebugging-usb-icon.svg", 114 l10nId: "toolbox.debugTargetInfo.connection.usb", 115 }; 116 case CONNECTION_TYPES.NETWORK: 117 return { 118 image: "chrome://devtools/skin/images/aboutdebugging-globe-icon.svg", 119 l10nId: "toolbox.debugTargetInfo.connection.network", 120 }; 121 default: 122 return {}; 123 } 124 } 125 126 getAssetsForDebugDescriptorType() { 127 const { descriptorType } = this.props.debugTargetData; 128 129 // TODO: https://bugzilla.mozilla.org/show_bug.cgi?id=1520723 130 // Show actual favicon (currently toolbox.target.activeTab.favicon 131 // is unpopulated) 132 const favicon = "chrome://devtools/skin/images/globe.svg"; 133 134 switch (descriptorType) { 135 case DESCRIPTOR_TYPES.EXTENSION: 136 return { 137 image: "chrome://devtools/skin/images/debugging-addons.svg", 138 l10nId: "toolbox.debugTargetInfo.targetType.extension", 139 }; 140 case DESCRIPTOR_TYPES.PROCESS: 141 return { 142 image: "chrome://devtools/skin/images/settings.svg", 143 l10nId: "toolbox.debugTargetInfo.targetType.process", 144 }; 145 case DESCRIPTOR_TYPES.TAB: 146 return { 147 image: favicon, 148 l10nId: "toolbox.debugTargetInfo.targetType.tab", 149 }; 150 case DESCRIPTOR_TYPES.WORKER: 151 return { 152 image: "chrome://devtools/skin/images/debugging-workers.svg", 153 l10nId: "toolbox.debugTargetInfo.targetType.worker", 154 }; 155 default: 156 return {}; 157 } 158 } 159 160 onChange({ target }) { 161 this.setState({ urlValue: target.value }); 162 } 163 164 onFocus({ target }) { 165 target.select(); 166 } 167 168 onSubmit(event) { 169 event.preventDefault(); 170 let url = this.state.urlValue; 171 172 if (!url || !url.length) { 173 return; 174 } 175 176 try { 177 // Get the URL from the fixup service: 178 const flags = Services.uriFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS; 179 const uriInfo = Services.uriFixup.getFixupURIInfo(url, flags); 180 url = uriInfo.fixedURI.spec; 181 } catch (ex) { 182 // The getFixupURIInfo service will throw an error if a malformed URI is 183 // produced from the input. 184 console.error(ex); 185 } 186 187 // Do not waitForLoad as we don't wait navigateTo to resolve anyway. 188 // Bug 1968023: navigateTo is flaky and sometimes never catches the 189 // STATE_STOP notification necessary for waitForLoad=true. 190 this.props.toolbox.commands.targetCommand.navigateTo( 191 url, 192 false /* waitForLoad */ 193 ); 194 } 195 196 shallRenderConnection() { 197 const { connectionType } = this.props.debugTargetData; 198 const renderableTypes = [CONNECTION_TYPES.USB, CONNECTION_TYPES.NETWORK]; 199 200 return renderableTypes.includes(connectionType); 201 } 202 203 renderConnection() { 204 const { connectionType } = this.props.debugTargetData; 205 const { image, l10nId } = this.getAssetsForConnectionType(); 206 207 return dom.span( 208 { 209 className: "iconized-label qa-connection-info", 210 }, 211 dom.img({ src: image, alt: `${connectionType} icon` }), 212 this.props.L10N.getStr(l10nId) 213 ); 214 } 215 216 renderRuntime() { 217 if ( 218 !this.props.debugTargetData.runtimeInfo || 219 (this.props.debugTargetData.connectionType === 220 CONNECTION_TYPES.THIS_FIREFOX && 221 this.props.debugTargetData.descriptorType === 222 DESCRIPTOR_TYPES.EXTENSION) 223 ) { 224 // Skip the runtime render if no runtimeInfo is available. 225 // Runtime info is retrieved from the remote-client-manager, which might not be 226 // setup if about:devtools-toolbox was not opened from about:debugging. 227 // 228 // Also skip the runtime if we are debugging firefox itself, mainly to save some space. 229 return null; 230 } 231 232 const { icon, deviceName } = this.props.debugTargetData.runtimeInfo; 233 234 return dom.span( 235 { 236 className: "iconized-label qa-runtime-info", 237 }, 238 dom.img({ src: icon, className: "channel-icon qa-runtime-icon" }), 239 dom.b({ className: "devtools-ellipsis-text" }, this.getRuntimeText()), 240 dom.span({ className: "devtools-ellipsis-text" }, deviceName) 241 ); 242 } 243 244 renderDescriptorName() { 245 const name = this.props.debugTargetData.descriptorName; 246 247 const { image, l10nId } = this.getAssetsForDebugDescriptorType(); 248 249 return dom.span( 250 { 251 className: "iconized-label debug-descriptor-title", 252 }, 253 dom.img({ src: image, alt: this.props.L10N.getStr(l10nId) }), 254 name 255 ? dom.b( 256 { className: "devtools-ellipsis-text qa-descriptor-title" }, 257 name 258 ) 259 : null 260 ); 261 } 262 263 renderTargetURI() { 264 const { descriptorType } = this.props.debugTargetData; 265 const { url } = this.props.toolbox.target; 266 const isWebExtension = descriptorType === DESCRIPTOR_TYPES.EXTENSION; 267 268 // Avoid displaying the target url for web extension as it is always 269 // the fallback document URL. Keeps rendering the url component 270 // as it use flex to align the "always on top" button on the right. 271 if (isWebExtension) { 272 return dom.span({ 273 className: "debug-target-url", 274 }); 275 } 276 277 const isURLEditable = descriptorType === DESCRIPTOR_TYPES.TAB; 278 279 return dom.span( 280 { 281 key: url, 282 className: "debug-target-url", 283 }, 284 isURLEditable 285 ? this.renderTargetInput(url) 286 : dom.span( 287 { className: "debug-target-url-readonly devtools-ellipsis-text" }, 288 url 289 ) 290 ); 291 } 292 293 renderTargetInput(url) { 294 return dom.form( 295 { 296 className: "debug-target-url-form", 297 onSubmit: this.onSubmit, 298 }, 299 dom.input({ 300 className: "devtools-textinput debug-target-url-input", 301 onChange: this.onChange, 302 onFocus: this.onFocus, 303 defaultValue: url, 304 }) 305 ); 306 } 307 308 renderAlwaysOnTopButton() { 309 // This is only displayed for local web extension debugging 310 const { descriptorType, connectionType } = this.props.debugTargetData; 311 const isLocalWebExtension = 312 descriptorType === DESCRIPTOR_TYPES.EXTENSION && 313 connectionType === CONNECTION_TYPES.THIS_FIREFOX; 314 if (!isLocalWebExtension) { 315 return []; 316 } 317 318 const checked = this.props.alwaysOnTop; 319 const toolboxFocused = this.props.focusedState; 320 return [ 321 Localized( 322 { 323 id: checked 324 ? "toolbox-always-on-top-enabled2" 325 : "toolbox-always-on-top-disabled2", 326 attrs: { title: true }, 327 }, 328 dom.button({ 329 className: 330 `toolbox-always-on-top` + 331 (checked ? " checked" : "") + 332 (toolboxFocused ? " toolbox-is-focused" : ""), 333 onClick: this.props.toggleAlwaysOnTop, 334 }) 335 ), 336 ]; 337 } 338 339 renderNavigationButton(detail) { 340 const { L10N } = this.props; 341 342 return dom.button( 343 { 344 className: `iconized-label navigation-button ${detail.className}`, 345 onClick: detail.onClick, 346 title: L10N.getStr(detail.l10nId), 347 }, 348 dom.img({ 349 src: detail.icon, 350 alt: L10N.getStr(detail.l10nId), 351 }) 352 ); 353 } 354 355 renderNavigation() { 356 const { debugTargetData } = this.props; 357 const { descriptorType } = debugTargetData; 358 359 if ( 360 descriptorType !== DESCRIPTOR_TYPES.TAB && 361 descriptorType !== DESCRIPTOR_TYPES.EXTENSION 362 ) { 363 return null; 364 } 365 366 const items = []; 367 368 // There is little value in exposing back/forward for WebExtensions 369 if ( 370 this.props.toolbox.target.getTrait("navigation") && 371 descriptorType === DESCRIPTOR_TYPES.TAB 372 ) { 373 items.push( 374 this.renderNavigationButton({ 375 className: "qa-back-button", 376 icon: "chrome://browser/skin/back.svg", 377 l10nId: "toolbox.debugTargetInfo.back", 378 onClick: () => this.props.toolbox.commands.targetCommand.goBack(), 379 }), 380 this.renderNavigationButton({ 381 className: "qa-forward-button", 382 icon: "chrome://browser/skin/forward.svg", 383 l10nId: "toolbox.debugTargetInfo.forward", 384 onClick: () => this.props.toolbox.commands.targetCommand.goForward(), 385 }) 386 ); 387 } 388 389 items.push( 390 this.renderNavigationButton({ 391 className: "qa-reload-button", 392 icon: "chrome://global/skin/icons/reload.svg", 393 l10nId: "toolbox.debugTargetInfo.reload", 394 onClick: () => this.props.toolbox.reload(), 395 }) 396 ); 397 398 return dom.div( 399 { 400 className: "debug-target-navigation", 401 }, 402 ...items 403 ); 404 } 405 406 render() { 407 return dom.header( 408 { 409 className: "debug-target-info qa-debug-target-info", 410 }, 411 this.shallRenderConnection() ? this.renderConnection() : null, 412 this.renderRuntime(), 413 this.renderDescriptorName(), 414 this.renderNavigation(), 415 this.renderTargetURI(), 416 ...this.renderAlwaysOnTopButton() 417 ); 418 } 419 } 420 421 module.exports = DebugTargetInfo;