summaryrefslogtreecommitdiffstats
path: root/devtools/client/netmonitor/src/components/previews
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /devtools/client/netmonitor/src/components/previews
parentInitial commit. (diff)
downloadthunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz
thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/netmonitor/src/components/previews')
-rw-r--r--devtools/client/netmonitor/src/components/previews/FontPreview.js135
-rw-r--r--devtools/client/netmonitor/src/components/previews/HtmlPreview.js75
-rw-r--r--devtools/client/netmonitor/src/components/previews/ImagePreview.js91
-rw-r--r--devtools/client/netmonitor/src/components/previews/SourcePreview.js178
-rw-r--r--devtools/client/netmonitor/src/components/previews/UrlPreview.js290
-rw-r--r--devtools/client/netmonitor/src/components/previews/moz.build11
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",
+)