UrlPreview.js (8435B)
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 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 const { 7 Component, 8 createFactory, 9 } = require("resource://devtools/client/shared/vendor/react.mjs"); 10 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs"); 11 12 const PropertiesView = createFactory( 13 require("resource://devtools/client/netmonitor/src/components/request-details/PropertiesView.js") 14 ); 15 const { 16 L10N, 17 } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); 18 const { 19 parseQueryString, 20 } = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); 21 22 const TreeRow = createFactory( 23 ChromeUtils.importESModule( 24 "resource://devtools/client/shared/components/tree/TreeRow.mjs", 25 { global: "current" } 26 ).default 27 ); 28 29 loader.lazyGetter(this, "MODE", function () { 30 return ChromeUtils.importESModule( 31 "resource://devtools/client/shared/components/reps/index.mjs" 32 ).MODE; 33 }); 34 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); 35 36 const { div, span, tr, td } = dom; 37 38 /** 39 * Url Preview Component 40 * This component is used to render urls. Its show both compact and destructured views 41 * of the url. Its takes a url and the http method as properties. 42 * 43 * Example Url: 44 * https://foo.com/bla?x=123&y=456&z=789&a=foo&a=bar 45 * 46 * Structure: 47 * { 48 * GET : { 49 * "scheme" : "https", 50 * "host" : "foo.com", 51 * "filename" : "bla", 52 * "query" : { 53 * "x": "123", 54 * "y": "456", 55 * "z": "789", 56 * "a": { 57 * "0": foo, 58 * "1": bar 59 * } 60 * }, 61 * "remote" : { 62 * "address" : "127.0.0.1:8080" 63 * } 64 * } 65 * } 66 */ 67 class UrlPreview extends Component { 68 static get propTypes() { 69 return { 70 url: PropTypes.string, 71 method: PropTypes.string, 72 address: PropTypes.string, 73 proxyStatus: PropTypes.string, 74 shouldExpandPreview: PropTypes.bool, 75 onTogglePreview: PropTypes.func, 76 }; 77 } 78 79 constructor(props) { 80 super(props); 81 this.parseUrl = this.parseUrl.bind(this); 82 this.renderValue = this.renderValue.bind(this); 83 } 84 85 shouldComponentUpdate(nextProps) { 86 return ( 87 nextProps.url !== this.props.url || 88 nextProps.method !== this.props.method || 89 nextProps.address !== this.props.address 90 ); 91 } 92 93 renderRow(props) { 94 const { 95 member: { name, level }, 96 } = props; 97 if ((name == "query" || name == "remote") && level == 1) { 98 return tr( 99 { key: name, className: "treeRow stringRow" }, 100 td( 101 { colSpan: 2, className: "splitter" }, 102 div({ className: "horizontal-splitter" }) 103 ) 104 ); 105 } 106 107 const customProps = { ...props }; 108 customProps.member.selected = false; 109 return TreeRow(customProps); 110 } 111 112 renderValue(props) { 113 const { 114 member: { level, open }, 115 value, 116 } = props; 117 if (level == 0) { 118 if (open) { 119 return ""; 120 } 121 const { scheme, host, filename, query } = value; 122 const queryParamNames = query ? Object.keys(query) : []; 123 // render collapsed url 124 return div( 125 { key: "url", className: "url" }, 126 span({ key: "url-scheme", className: "url-scheme" }, `${scheme}://`), 127 span({ key: "url-host", className: "url-host" }, `${host}`), 128 span({ key: "url-filename", className: "url-filename" }, `${filename}`), 129 !!queryParamNames.length && 130 span({ key: "url-ques", className: "url-chars" }, "?"), 131 132 queryParamNames.map((name, index) => { 133 if (Array.isArray(query[name])) { 134 return query[name].map((item, queryIndex) => { 135 return span( 136 { 137 key: `url-params-${name}${queryIndex}`, 138 className: "url-params", 139 }, 140 span( 141 { 142 key: `url-params${name}${queryIndex}-name`, 143 className: "url-params-name", 144 }, 145 `${name}` 146 ), 147 span( 148 { 149 key: `url-chars-${name}${queryIndex}-equals`, 150 className: "url-chars", 151 }, 152 "=" 153 ), 154 span( 155 { 156 key: `url-params-${name}${queryIndex}-value`, 157 className: "url-params-value", 158 }, 159 `${item}` 160 ), 161 (query[name].length - 1 !== queryIndex || 162 queryParamNames.length - 1 !== index) && 163 span({ key: "url-amp", className: "url-chars" }, "&") 164 ); 165 }); 166 } 167 168 return span( 169 { key: `url-params-${name}`, className: "url-params" }, 170 span( 171 { key: "url-params-name", className: "url-params-name" }, 172 `${name}` 173 ), 174 span({ key: "url-chars-equals", className: "url-chars" }, "="), 175 span( 176 { key: "url-params-value", className: "url-params-value" }, 177 `${query[name]}` 178 ), 179 queryParamNames.length - 1 !== index && 180 span({ key: "url-amp", className: "url-chars" }, "&") 181 ); 182 }) 183 ); 184 } 185 if (typeof value !== "string") { 186 // the query node would be an object 187 if (level == 0) { 188 return ""; 189 } 190 // for arrays (multival) 191 return "[...]"; 192 } 193 194 return value; 195 } 196 197 parseUrl(url) { 198 const { method, address, proxyStatus } = this.props; 199 const { host, protocol, pathname, search } = new URL(url); 200 201 const urlObject = { 202 [method]: { 203 scheme: protocol.replace(":", ""), 204 host, 205 filename: pathname, 206 }, 207 }; 208 209 const expandedNodes = new Set(); 210 211 // check and add query parameters 212 if (search.length) { 213 const params = parseQueryString(search); 214 // make sure the query node is always expanded 215 expandedNodes.add(`/${method}/query`); 216 urlObject[method].query = params.reduce((map, obj) => { 217 const value = map[obj.name]; 218 if (value || value === "") { 219 if (typeof value !== "object") { 220 expandedNodes.add(`/${method}/query/${obj.name}`); 221 map[obj.name] = [value]; 222 } 223 map[obj.name].push(obj.value); 224 } else { 225 map[obj.name] = obj.value; 226 } 227 return map; 228 }, Object.create(null)); 229 } 230 231 if (address) { 232 // makes sure the remote address section is expanded 233 expandedNodes.add(`/${method}/remote`); 234 urlObject[method].remote = { 235 [L10N.getStr( 236 proxyStatus 237 ? "netmonitor.headers.proxyAddress" 238 : "netmonitor.headers.address" 239 )]: address, 240 }; 241 } 242 243 return { 244 urlObject, 245 expandedNodes, 246 }; 247 } 248 249 render() { 250 const { 251 url, 252 method, 253 shouldExpandPreview = false, 254 onTogglePreview, 255 } = this.props; 256 257 const { urlObject, expandedNodes } = this.parseUrl(url); 258 259 if (shouldExpandPreview) { 260 expandedNodes.add(`/${method}`); 261 } 262 263 return div( 264 { className: "url-preview" }, 265 PropertiesView({ 266 object: urlObject, 267 useQuotes: true, 268 defaultSelectFirstNode: false, 269 mode: MODE.TINY, 270 expandedNodes, 271 renderRow: this.renderRow, 272 renderValue: this.renderValue, 273 enableInput: false, 274 onClickRow: (path, evt, member) => { 275 // Only track when the root is toggled 276 // as all the others are always expanded by 277 // default. 278 if (path == `/${method}`) { 279 onTogglePreview(!member.open); 280 } 281 }, 282 contextMenuFormatters: { 283 copyFormatter: (member, baseCopyFormatter) => { 284 const { value, level, hasChildren } = member; 285 if (hasChildren && level == 0) { 286 const { scheme, filename, host, query } = value; 287 return `${scheme}://${host}${filename}${ 288 query ? "?" + new URLSearchParams(query).toString() : "" 289 }`; 290 } 291 return baseCopyFormatter(member); 292 }, 293 }, 294 }) 295 ); 296 } 297 } 298 299 module.exports = UrlPreview;