diff options
Diffstat (limited to 'devtools/client/netmonitor/src/components/previews')
6 files changed, 780 insertions, 0 deletions
diff --git a/devtools/client/netmonitor/src/components/previews/FontPreview.js b/devtools/client/netmonitor/src/components/previews/FontPreview.js new file mode 100644 index 0000000000..2692fdefa0 --- /dev/null +++ b/devtools/client/netmonitor/src/components/previews/FontPreview.js @@ -0,0 +1,135 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; +const { + gDevTools, +} = require("resource://devtools/client/framework/devtools.js"); +const { + Component, +} = require("resource://devtools/client/shared/vendor/react.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + L10N, +} = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); + +const { + div, + img, +} = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +const { getColor } = require("resource://devtools/client/shared/theme.js"); + +const FONT_NAME = L10N.getStr("netmonitor.response.name"); +const FONT_MIME_TYPE = L10N.getStr("netmonitor.response.mime"); +const FONT_PREVIEW_FAILED = L10N.getStr( + "netmonitor.response.fontPreviewFailed" +); + +const FONT_PREVIEW_TEXT = + "ABCDEFGHIJKLM\nNOPQRSTUVWXYZ\nabcdefghijklm\nnopqrstuvwxyz\n0123456789"; + +class FontPreview extends Component { + static get propTypes() { + return { + connector: PropTypes.object.isRequired, + mimeType: PropTypes.string, + url: PropTypes.string, + }; + } + + constructor(props) { + super(props); + + this.state = { + name: "", + dataURL: "", + }; + + this.onThemeChanged = this.onThemeChanged.bind(this); + } + + componentDidMount() { + this.getPreview(); + + // Listen for theme changes as the color of the preview depends on the theme + gDevTools.on("theme-switched", this.onThemeChanged); + } + + componentDidUpdate(prevProps) { + const { url } = this.props; + if (prevProps.url !== url) { + this.getPreview(); + } + } + + componentWillUnmount() { + gDevTools.off("theme-switched", this.onThemeChanged); + } + + /** + * Handler for the "theme-switched" event. + */ + onThemeChanged(frame) { + if (frame === window) { + this.getPreview(); + } + } + + /** + * Generate the font preview and receives information about the font. + */ + async getPreview() { + const { connector } = this.props; + + const toolbox = connector.getToolbox(); + const inspectorFront = await toolbox.target.getFront("inspector"); + const { pageStyle } = inspectorFront; + const pageFontFaces = await pageStyle.getAllUsedFontFaces({ + includePreviews: true, + includeVariations: false, + previewText: FONT_PREVIEW_TEXT, + previewFillStyle: getColor("body-color"), + }); + + const fontFace = pageFontFaces.find( + pageFontFace => pageFontFace.URI === this.props.url + ); + + this.setState({ + name: fontFace?.name ?? "", + dataURL: (await fontFace?.preview.data.string()) ?? "", + }); + } + + render() { + const { mimeType } = this.props; + const { name, dataURL } = this.state; + + if (dataURL === "") { + return div({ className: "empty-notice" }, FONT_PREVIEW_FAILED); + } + + return div( + { className: "panel-container response-font-box devtools-monospace" }, + img({ + className: "response-font", + src: dataURL, + alt: "", + }), + div( + { className: "response-summary" }, + div({ className: "tabpanel-summary-label" }, FONT_NAME), + div({ className: "tabpanel-summary-value" }, name) + ), + div( + { className: "response-summary" }, + div({ className: "tabpanel-summary-label" }, FONT_MIME_TYPE), + div({ className: "tabpanel-summary-value" }, mimeType) + ) + ); + } +} + +module.exports = FontPreview; diff --git a/devtools/client/netmonitor/src/components/previews/HtmlPreview.js b/devtools/client/netmonitor/src/components/previews/HtmlPreview.js new file mode 100644 index 0000000000..ab8c24fac0 --- /dev/null +++ b/devtools/client/netmonitor/src/components/previews/HtmlPreview.js @@ -0,0 +1,75 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + Component, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +/* + * Response preview component + * Display HTML content within a sandbox enabled iframe + */ +class HTMLPreview extends Component { + static get propTypes() { + return { + responseContent: PropTypes.object.isRequired, + }; + } + + componentDidMount() { + const { container } = this.refs; + const iframe = container.ownerDocument.createXULElement("iframe"); + this.iframe = iframe; + iframe.setAttribute("type", "content"); + iframe.setAttribute("remote", "true"); + + // For some reason, when we try to select some text, + // a drag of the whole page is initiated. + // Workaround this by canceling any start of drag. + iframe.addEventListener("dragstart", e => e.preventDefault(), { + capture: true, + }); + + // Bug 1800916 allow interaction with the preview page until + // we find a way to prevent navigation without preventing copy paste from it. + // + // iframe.addEventListener("mousedown", e => e.preventDefault(), { + // capture: true, + // }); + container.appendChild(iframe); + + // browsingContext attribute is only available after the iframe + // is attached to the DOM Tree. + iframe.browsingContext.allowJavascript = false; + + this.#updatePreview(); + } + + componentDidUpdate() { + this.#updatePreview(); + } + + componentWillUnmount() { + this.iframe.remove(); + } + + #updatePreview() { + const { responseContent } = this.props; + const htmlBody = responseContent ? responseContent.content.text : ""; + this.iframe.setAttribute( + "src", + "data:text/html;charset=UTF-8," + encodeURIComponent(htmlBody) + ); + } + + render() { + return dom.div({ className: "html-preview", ref: "container" }); + } +} + +module.exports = HTMLPreview; diff --git a/devtools/client/netmonitor/src/components/previews/ImagePreview.js b/devtools/client/netmonitor/src/components/previews/ImagePreview.js new file mode 100644 index 0000000000..003a7ec38e --- /dev/null +++ b/devtools/client/netmonitor/src/components/previews/ImagePreview.js @@ -0,0 +1,91 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + Component, +} = require("resource://devtools/client/shared/vendor/react.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + L10N, +} = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); + +const { + div, + img, +} = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +const { + formDataURI, + getUrlBaseName, +} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); + +const RESPONSE_IMG_NAME = L10N.getStr("netmonitor.response.name"); +const RESPONSE_IMG_DIMENSIONS = L10N.getStr("netmonitor.response.dimensions"); +const RESPONSE_IMG_MIMETYPE = L10N.getStr("netmonitor.response.mime"); + +class ImagePreview extends Component { + static get propTypes() { + return { + mimeType: PropTypes.string, + encoding: PropTypes.string, + text: PropTypes.string, + url: PropTypes.string, + }; + } + + constructor(props) { + super(props); + + this.state = { + dimensions: { + width: 0, + height: 0, + }, + }; + + this.updateDimensions = this.updateDimensions.bind(this); + } + + updateDimensions({ target }) { + this.setState({ + dimensions: { + width: target.naturalWidth, + height: target.naturalHeight, + }, + }); + } + + render() { + const { mimeType, encoding, text, url } = this.props; + const { width, height } = this.state.dimensions; + + return div( + { className: "panel-container response-image-box devtools-monospace" }, + img({ + className: "response-image", + src: formDataURI(mimeType, encoding, text), + onLoad: this.updateDimensions, + }), + div( + { className: "response-summary" }, + div({ className: "tabpanel-summary-label" }, RESPONSE_IMG_NAME), + div({ className: "tabpanel-summary-value" }, getUrlBaseName(url)) + ), + div( + { className: "response-summary" }, + div({ className: "tabpanel-summary-label" }, RESPONSE_IMG_DIMENSIONS), + div({ className: "tabpanel-summary-value" }, `${width} × ${height}`) + ), + div( + { className: "response-summary" }, + div({ className: "tabpanel-summary-label" }, RESPONSE_IMG_MIMETYPE), + div({ className: "tabpanel-summary-value" }, mimeType) + ) + ); + } +} + +module.exports = ImagePreview; diff --git a/devtools/client/netmonitor/src/components/previews/SourcePreview.js b/devtools/client/netmonitor/src/components/previews/SourcePreview.js new file mode 100644 index 0000000000..7e9d011bad --- /dev/null +++ b/devtools/client/netmonitor/src/components/previews/SourcePreview.js @@ -0,0 +1,178 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; +const { + Component, +} = require("resource://devtools/client/shared/vendor/react.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + connect, +} = require("resource://devtools/client/shared/redux/visibility-handler-connect.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const Editor = require("resource://devtools/client/shared/sourceeditor/editor.js"); +const { + setTargetSearchResult, +} = require("resource://devtools/client/netmonitor/src/actions/search.js"); +const { div } = dom; +/** + * CodeMirror editor as a React component + */ +class SourcePreview extends Component { + static get propTypes() { + return { + // Source editor syntax highlight mode, which is a mime type defined in CodeMirror + mode: PropTypes.string, + // Source editor content + text: PropTypes.string, + // Search result text to select + targetSearchResult: PropTypes.object, + // Reset target search result that has been used for navigation in this panel. + // This is done to avoid second navigation the next time. + resetTargetSearchResult: PropTypes.func, + }; + } + + componentDidMount() { + const { mode, text } = this.props; + this.loadEditor(mode, text); + } + + shouldComponentUpdate(nextProps) { + return ( + nextProps.mode !== this.props.mode || + nextProps.text !== this.props.text || + nextProps.targetSearchResult !== this.props.targetSearchResult + ); + } + + componentDidUpdate(prevProps) { + const { mode, targetSearchResult, text } = this.props; + + if (prevProps.text !== text) { + // When updating from editor to editor + this.updateEditor(mode, text); + } else if (prevProps.targetSearchResult !== targetSearchResult) { + this.findSearchResult(); + } + } + + componentWillUnmount() { + this.unloadEditor(); + } + + loadEditor(mode, text) { + this.editor = new Editor({ + lineNumbers: true, + lineWrapping: false, + mode: null, // Disable auto syntax detection, but then we set mode asynchronously + readOnly: true, + theme: "mozilla", + value: text, + }); + + // Delay to CodeMirror initialization content to prevent UI freezing + this.editorTimeout = setTimeout(() => { + this.editorTimeout = null; + this.editor.appendToLocalElement(this.refs.editorElement); + + // CodeMirror's setMode() (syntax highlight) is the performance bottleneck when + // processing large content, so we enable it asynchronously within the setTimeout + // to avoid UI blocking. (rendering source code -> drawing syntax highlight) + this.editorSetModeTimeout = setTimeout(() => { + this.editorSetModeTimeout = null; + this.editor.setMode(mode); + this.findSearchResult(); + }); + }); + } + + updateEditor(mode, text) { + // Reset the existed 'mode' attribute in order to make setText() process faster + // to prevent drawing unnecessary syntax highlight. + if (this?.editor?.hasCodeMirror) { + this.editor.setMode(null); + this.editor.setText(text); + } + + if (this.editorSetModeTimeout) { + clearTimeout(this.editorSetModeTimeout); + } + + // CodeMirror's setMode() (syntax highlight) is the performance bottleneck when + // processing large content, so we enable it asynchronously within the setTimeout + // to avoid UI blocking. (rendering source code -> drawing syntax highlight) + this.editorSetModeTimeout = setTimeout(() => { + this.editorSetModeTimeout = null; + this.editor.setMode(mode); + this.findSearchResult(); + }); + } + + unloadEditor() { + clearTimeout(this.editorTimeout); + clearTimeout(this.editorSetModeTimeout); + if (this.editor) { + this.editor.destroy(); + this.editor = null; + } + } + + findSearchResult() { + const { targetSearchResult, resetTargetSearchResult } = this.props; + + if (targetSearchResult?.line) { + const { line } = targetSearchResult; + // scroll the editor to center the line + // with the target search result + if (this.editor) { + this.editor.setCursor({ line: line - 1 }, "center"); + } + } + + resetTargetSearchResult(); + } + + // Scroll to specified line if the user clicks on search results. + scrollToLine(element) { + const { targetSearchResult, resetTargetSearchResult } = this.props; + + // The following code is responsible for scrolling given line + // to visible view-port. + // It gets the <div> child element representing the target + // line (by index) and uses `scrollIntoView` API to make sure + // it's visible to the user. + if (element && targetSearchResult && targetSearchResult.line) { + const child = element.children[targetSearchResult.line - 1]; + if (child) { + const range = document.createRange(); + range.selectNode(child); + document.getSelection().addRange(range); + child.scrollIntoView({ block: "center" }); + } + resetTargetSearchResult(); + } + } + + renderEditor() { + return div( + { className: "editor-row-container" }, + div({ + ref: "editorElement", + className: "source-editor-mount devtools-monospace", + }) + ); + } + + render() { + return div( + { key: "EDITOR_CONFIG", className: "editor-row-container" }, + this.renderEditor() + ); + } +} + +module.exports = connect(null, dispatch => ({ + resetTargetSearchResult: () => dispatch(setTargetSearchResult(null)), +}))(SourcePreview); diff --git a/devtools/client/netmonitor/src/components/previews/UrlPreview.js b/devtools/client/netmonitor/src/components/previews/UrlPreview.js new file mode 100644 index 0000000000..c20762912f --- /dev/null +++ b/devtools/client/netmonitor/src/components/previews/UrlPreview.js @@ -0,0 +1,290 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; +const { + Component, + createFactory, +} = require("resource://devtools/client/shared/vendor/react.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +const PropertiesView = createFactory( + require("resource://devtools/client/netmonitor/src/components/request-details/PropertiesView.js") +); +const { + L10N, +} = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); +const { + parseQueryString, +} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); + +const TreeRow = createFactory( + require("resource://devtools/client/shared/components/tree/TreeRow.js") +); + +loader.lazyGetter(this, "MODE", function () { + return require("resource://devtools/client/shared/components/reps/index.js") + .MODE; +}); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +const { div, span, tr, td } = dom; + +/** + * Url Preview Component + * This component is used to render urls. Its show both compact and destructured views + * of the url. Its takes a url and the http method as properties. + * + * Example Url: + * https://foo.com/bla?x=123&y=456&z=789&a=foo&a=bar + * + * Structure: + * { + * GET : { + * "scheme" : "https", + * "host" : "foo.com", + * "filename" : "bla", + * "query" : { + * "x": "123", + * "y": "456", + * "z": "789", + * "a": { + * "0": foo, + * "1": bar + * } + * }, + * "remote" : { + * "address" : "127.0.0.1:8080" + * } + * } + * } + */ +class UrlPreview extends Component { + static get propTypes() { + return { + url: PropTypes.string, + method: PropTypes.string, + address: PropTypes.string, + shouldExpandPreview: PropTypes.bool, + onTogglePreview: PropTypes.func, + }; + } + + constructor(props) { + super(props); + this.parseUrl = this.parseUrl.bind(this); + this.renderValue = this.renderValue.bind(this); + } + + shouldComponentUpdate(nextProps) { + return ( + nextProps.url !== this.props.url || + nextProps.method !== this.props.method || + nextProps.address !== this.props.address + ); + } + + renderRow(props) { + const { + member: { name, level }, + } = props; + if ((name == "query" || name == "remote") && level == 1) { + return tr( + { key: name, className: "treeRow stringRow" }, + td( + { colSpan: 2, className: "splitter" }, + div({ className: "horizontal-splitter" }) + ) + ); + } + + const customProps = { ...props }; + customProps.member.selected = false; + return TreeRow(customProps); + } + + renderValue(props) { + const { + member: { level, open }, + value, + } = props; + if (level == 0) { + if (open) { + return ""; + } + const { scheme, host, filename, query } = value; + const queryParamNames = query ? Object.keys(query) : []; + // render collapsed url + return div( + { key: "url", className: "url" }, + span({ key: "url-scheme", className: "url-scheme" }, `${scheme}://`), + span({ key: "url-host", className: "url-host" }, `${host}`), + span({ key: "url-filename", className: "url-filename" }, `${filename}`), + !!queryParamNames.length && + span({ key: "url-ques", className: "url-chars" }, "?"), + + queryParamNames.map((name, index) => { + if (Array.isArray(query[name])) { + return query[name].map((item, queryIndex) => { + return span( + { + key: `url-params-${name}${queryIndex}`, + className: "url-params", + }, + span( + { + key: `url-params${name}${queryIndex}-name`, + className: "url-params-name", + }, + `${name}` + ), + span( + { + key: `url-chars-${name}${queryIndex}-equals`, + className: "url-chars", + }, + "=" + ), + span( + { + key: `url-params-${name}${queryIndex}-value`, + className: "url-params-value", + }, + `${item}` + ), + (query[name].length - 1 !== queryIndex || + queryParamNames.length - 1 !== index) && + span({ key: "url-amp", className: "url-chars" }, "&") + ); + }); + } + + return span( + { key: `url-params-${name}`, className: "url-params" }, + span( + { key: "url-params-name", className: "url-params-name" }, + `${name}` + ), + span({ key: "url-chars-equals", className: "url-chars" }, "="), + span( + { key: "url-params-value", className: "url-params-value" }, + `${query[name]}` + ), + queryParamNames.length - 1 !== index && + span({ key: "url-amp", className: "url-chars" }, "&") + ); + }) + ); + } + if (typeof value !== "string") { + // the query node would be an object + if (level == 0) { + return ""; + } + // for arrays (multival) + return "[...]"; + } + + return value; + } + + parseUrl(url) { + const { method, address } = this.props; + const { host, protocol, pathname, search } = new URL(url); + + const urlObject = { + [method]: { + scheme: protocol.replace(":", ""), + host, + filename: pathname, + }, + }; + + const expandedNodes = new Set(); + + // check and add query parameters + if (search.length) { + const params = parseQueryString(search); + // make sure the query node is always expanded + expandedNodes.add(`/${method}/query`); + urlObject[method].query = params.reduce((map, obj) => { + const value = map[obj.name]; + if (value || value === "") { + if (typeof value !== "object") { + expandedNodes.add(`/${method}/query/${obj.name}`); + map[obj.name] = [value]; + } + map[obj.name].push(obj.value); + } else { + map[obj.name] = obj.value; + } + return map; + }, Object.create(null)); + } + + if (address) { + // makes sure the remote adress section is expanded + expandedNodes.add(`/${method}/remote`); + urlObject[method].remote = { + [L10N.getStr("netmonitor.headers.address")]: address, + }; + } + + return { + urlObject, + expandedNodes, + }; + } + + render() { + const { + url, + method, + shouldExpandPreview = false, + onTogglePreview, + } = this.props; + + const { urlObject, expandedNodes } = this.parseUrl(url); + + if (shouldExpandPreview) { + expandedNodes.add(`/${method}`); + } + + return div( + { className: "url-preview" }, + PropertiesView({ + object: urlObject, + useQuotes: true, + defaultSelectFirstNode: false, + mode: MODE.TINY, + expandedNodes, + renderRow: this.renderRow, + renderValue: this.renderValue, + enableInput: false, + onClickRow: (path, evt, member) => { + // Only track when the root is toggled + // as all the others are always expanded by + // default. + if (path == `/${method}`) { + onTogglePreview(!member.open); + } + }, + contextMenuFormatters: { + copyFormatter: (member, baseCopyFormatter) => { + const { value, level, hasChildren } = member; + if (hasChildren && level == 0) { + const { scheme, filename, host, query } = value; + return `${scheme}://${host}${filename}${ + query ? "?" + new URLSearchParams(query).toString() : "" + }`; + } + return baseCopyFormatter(member); + }, + }, + }) + ); + } +} + +module.exports = UrlPreview; diff --git a/devtools/client/netmonitor/src/components/previews/moz.build b/devtools/client/netmonitor/src/components/previews/moz.build new file mode 100644 index 0000000000..0252ec3246 --- /dev/null +++ b/devtools/client/netmonitor/src/components/previews/moz.build @@ -0,0 +1,11 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + "FontPreview.js", + "HtmlPreview.js", + "ImagePreview.js", + "SourcePreview.js", + "UrlPreview.js", +) |