PropertiesView.js (7543B)
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 /* eslint-disable react/prop-types */ 6 7 "use strict"; 8 9 const { 10 Component, 11 createFactory, 12 } = require("resource://devtools/client/shared/vendor/react.mjs"); 13 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); 14 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs"); 15 const { 16 connect, 17 } = require("resource://devtools/client/shared/vendor/react-redux.js"); 18 const { 19 setTargetSearchResult, 20 } = require("resource://devtools/client/netmonitor/src/actions/search.js"); 21 22 // Components 23 const TreeViewClass = ChromeUtils.importESModule( 24 "resource://devtools/client/shared/components/tree/TreeView.mjs" 25 ).default; 26 const TreeView = createFactory(TreeViewClass); 27 const PropertiesViewContextMenu = require("resource://devtools/client/netmonitor/src/widgets/PropertiesViewContextMenu.js"); 28 29 loader.lazyGetter(this, "Rep", function () { 30 return ChromeUtils.importESModule( 31 "resource://devtools/client/shared/components/reps/index.mjs" 32 ).REPS.Rep; 33 }); 34 loader.lazyGetter(this, "MODE", function () { 35 return ChromeUtils.importESModule( 36 "resource://devtools/client/shared/components/reps/index.mjs" 37 ).MODE; 38 }); 39 40 // Constants 41 const { 42 AUTO_EXPAND_MAX_LEVEL, 43 AUTO_EXPAND_MAX_NODES, 44 } = require("resource://devtools/client/netmonitor/src/constants.js"); 45 46 const { div } = dom; 47 48 /** 49 * Properties View component 50 * A scrollable tree view component which provides some useful features for 51 * representing object properties. 52 * 53 * Tree view 54 * Rep 55 */ 56 class PropertiesView extends Component { 57 static get propTypes() { 58 return { 59 object: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), 60 provider: PropTypes.object, 61 enableInput: PropTypes.bool, 62 expandableStrings: PropTypes.bool, 63 expandedNodes: PropTypes.object, 64 useBaseTreeViewExpand: PropTypes.bool, 65 filterText: PropTypes.string, 66 cropLimit: PropTypes.number, 67 targetSearchResult: PropTypes.object, 68 resetTargetSearchResult: PropTypes.func, 69 selectPath: PropTypes.func, 70 mode: PropTypes.symbol, 71 defaultSelectFirstNode: PropTypes.bool, 72 useQuotes: PropTypes.bool, 73 onClickRow: PropTypes.func, 74 contextMenuFormatters: PropTypes.object, 75 }; 76 } 77 78 static get defaultProps() { 79 return { 80 enableInput: true, 81 enableFilter: true, 82 expandableStrings: false, 83 cropLimit: 1024, 84 useQuotes: true, 85 contextMenuFormatters: {}, 86 useBaseTreeViewExpand: false, 87 }; 88 } 89 90 constructor(props) { 91 super(props); 92 this.onFilter = this.onFilter.bind(this); 93 this.renderValueWithRep = this.renderValueWithRep.bind(this); 94 this.getSelectedPath = this.getSelectedPath.bind(this); 95 96 this.expandedNodes = new Set(); 97 } 98 99 /** 100 * Update only if: 101 * 1) The rendered object has changed 102 * 2) The filter text has changed 103 * 3) The user selected another search result target. 104 */ 105 shouldComponentUpdate(nextProps) { 106 return ( 107 this.props.object !== nextProps.object || 108 this.props.filterText !== nextProps.filterText || 109 (this.props.targetSearchResult !== nextProps.targetSearchResult && 110 nextProps.targetSearchResult !== null) 111 ); 112 } 113 114 onFilter(props) { 115 const { name, value } = props; 116 const { filterText } = this.props; 117 118 if (!filterText) { 119 return true; 120 } 121 122 const jsonString = JSON.stringify({ [name]: value }).toLowerCase(); 123 return jsonString.includes(filterText.toLowerCase()); 124 } 125 126 getSelectedPath(targetSearchResult) { 127 if (!targetSearchResult) { 128 return null; 129 } 130 131 return `/${targetSearchResult.label}`; 132 } 133 134 /** 135 * If target is selected, let's scroll the content 136 * so the property is visible. This is used for search result navigation, 137 * which happens when the user clicks on a search result. 138 */ 139 scrollSelectedIntoView() { 140 const { targetSearchResult, resetTargetSearchResult, selectPath } = 141 this.props; 142 if (!targetSearchResult) { 143 return; 144 } 145 146 const path = 147 typeof selectPath == "function" 148 ? selectPath(targetSearchResult) 149 : this.getSelectedPath(targetSearchResult); 150 const element = document.getElementById(path); 151 if (element) { 152 element.scrollIntoView({ block: "center" }); 153 } 154 155 resetTargetSearchResult(); 156 } 157 158 onContextMenuRow(member, evt) { 159 evt.preventDefault(); 160 161 const { object } = member; 162 163 // Select the right clicked row 164 this.selectRow({ props: { member } }); 165 166 // if data exists and can be copied, then show the contextmenu 167 if (typeof object === "object") { 168 if (!this.contextMenu) { 169 this.contextMenu = new PropertiesViewContextMenu({ 170 customFormatters: this.props.contextMenuFormatters, 171 }); 172 } 173 this.contextMenu.open(evt, window.getSelection(), { 174 member, 175 object: this.props.object, 176 }); 177 } 178 } 179 180 renderValueWithRep(props) { 181 const { member } = props; 182 183 /* Hide strings with following conditions 184 * - the `value` object has a `value` property (only happens in Cookies panel) 185 */ 186 if (typeof member.value === "object" && member.value?.value) { 187 return null; 188 } 189 190 return Rep( 191 Object.assign(props, { 192 // FIXME: A workaround for the issue in StringRep 193 // Force StringRep to crop the text every time 194 member: Object.assign({}, member, { open: false }), 195 mode: this.props.mode || MODE.TINY, 196 cropLimit: this.props.cropLimit, 197 noGrip: true, 198 }) 199 ); 200 } 201 202 render() { 203 const { 204 useBaseTreeViewExpand, 205 expandedNodes, 206 object, 207 renderValue, 208 targetSearchResult, 209 selectPath, 210 } = this.props; 211 212 let currentExpandedNodes; 213 // In the TreeView, when the component is re-rendered 214 // the state of `expandedNodes` is persisted by default 215 // e.g. when you open a node and filter the properties list, 216 // the node remains open. 217 // We have the prop `useBaseTreeViewExpand` to flag when we want to use 218 // this functionality or not. 219 if (!useBaseTreeViewExpand) { 220 currentExpandedNodes = 221 expandedNodes || 222 TreeViewClass.getExpandedNodes(object, { 223 maxLevel: AUTO_EXPAND_MAX_LEVEL, 224 maxNodes: AUTO_EXPAND_MAX_NODES, 225 }); 226 } else { 227 // Ensure passing a stable expanded Set, 228 // so that TreeView doesn't reset to default prop's Set 229 // on each new received props. 230 currentExpandedNodes = this.expandedNodes; 231 } 232 return div( 233 { className: "properties-view" }, 234 div( 235 { className: "tree-container" }, 236 TreeView({ 237 ...this.props, 238 ref: () => this.scrollSelectedIntoView(), 239 columns: [{ id: "value", width: "100%" }], 240 241 expandedNodes: currentExpandedNodes, 242 243 onFilter: props => this.onFilter(props), 244 renderValue: renderValue || this.renderValueWithRep, 245 onContextMenuRow: this.onContextMenuRow, 246 selected: 247 typeof selectPath == "function" 248 ? selectPath(targetSearchResult) 249 : this.getSelectedPath(targetSearchResult), 250 }) 251 ) 252 ); 253 } 254 } 255 256 module.exports = connect(null, dispatch => ({ 257 resetTargetSearchResult: () => dispatch(setTargetSearchResult(null)), 258 }))(PropertiesView);