Files
sousa-gecko/devtools/client/jsonview/components/JsonPanel.mjs
T

277 lines
7.6 KiB
JavaScript

/* 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/. */
import {
createFactory,
Component,
} from "resource://devtools/client/shared/vendor/react.mjs";
import * as dom from "resource://devtools/client/shared/vendor/react-dom-factories.mjs";
import PropTypes from "resource://devtools/client/shared/vendor/react-prop-types.mjs";
import { createFactories } from "resource://devtools/client/shared/react-utils.mjs";
import TreeViewClass from "resource://devtools/client/shared/components/tree/TreeView.mjs";
import { BucketProperty } from "resource://devtools/client/shared/components/tree/ObjectProvider.mjs";
import JsonToolbarClass from "resource://devtools/client/jsonview/components/JsonToolbar.mjs";
import JsonBreadcrumbClass from "resource://devtools/client/jsonview/components/JsonBreadcrumb.mjs";
import {
JSON_NUMBER,
MODE,
} from "resource://devtools/client/shared/components/reps/reps/constants.mjs";
import { Rep } from "resource://devtools/client/shared/components/reps/reps/rep.mjs";
const TreeView = createFactory(TreeViewClass);
const { JsonToolbar } = createFactories(JsonToolbarClass);
const { JsonBreadcrumb } = createFactories(JsonBreadcrumbClass);
const { div } = dom;
const MAX_STRING_LENGTH = 250;
function isObject(value) {
return Object(value) === value;
}
/**
* This template represents the 'JSON' panel. The panel is
* responsible for rendering an expandable tree that allows simple
* inspection of JSON structure.
*/
class JsonPanel extends Component {
static get propTypes() {
return {
data: PropTypes.oneOfType([
PropTypes.string,
PropTypes.array,
PropTypes.object,
PropTypes.bool,
PropTypes.number,
]),
dataSize: PropTypes.number,
expandedNodes: PropTypes.instanceOf(Set),
searchFilter: PropTypes.string,
actions: PropTypes.object,
};
}
/**
* Splits a path string by unescaped forward slashes (`/`).
*
* Escaped slashes (`\/`) are preserved as literal `/` characters
* in the resulting segments. Escape characters (`\`) are removed.
*
* Examples:
* splitPath("a/b/c") → ["a", "b", "c"]
* splitPath("a\\/b/c") → ["a/b", "c"]
* splitPath("/a/b/") → ["a", "b"]
*
* @param {string} path
* The input path string. Forward slashes can be escaped using `\`.
*
* @returns {string[]}
* An array of non-empty path segments with escape characters removed.
*/
static splitPath(path) {
return path
.split(/(?<!\\)\//)
.map(segment => segment.replace(/\\(.)/g, "$1"))
.filter(_s => _s.length);
}
constructor(props) {
super(props);
this.state = {};
this.onKeyPress = this.onKeyPress.bind(this);
this.onFilter = this.onFilter.bind(this);
this.renderValue = this.renderValue.bind(this);
this.renderTree = this.renderTree.bind(this);
this.onRowSelected = this.onRowSelected.bind(this);
this.updateBreadcrumbs = this.updateBreadcrumbs.bind(this);
}
componentDidMount() {
document.addEventListener("keypress", this.onKeyPress, true);
document.getElementById("json-scrolling-panel").focus();
}
componentWillUnmount() {
document.removeEventListener("keypress", this.onKeyPress, true);
}
onKeyPress() {
// XXX shortcut for focusing the Filter field (see Bug 1178771).
}
onRowSelected(selectedRowPath) {
if (!selectedRowPath || typeof selectedRowPath !== "string") {
return;
}
const rowPathParts = JsonPanel.splitPath(selectedRowPath);
this.updateBreadcrumbs(rowPathParts);
}
/**
* @param {string[]} rowPathParts
*/
updateBreadcrumbs(rowPathParts) {
let jsonData = this.props.data;
const breadcrumbs = [];
for (let i = 0; i < rowPathParts.length; i++) {
if (typeof jsonData !== "object") {
break;
}
const key = rowPathParts[i];
// Skip bucket range segments like "[0…99]"
if (jsonData[key] === undefined && Array.isArray(jsonData)) {
continue;
}
jsonData = jsonData[key];
breadcrumbs.push({ type: getJsonValueType(jsonData), text: key });
}
if (breadcrumbs.length) {
this.setState({ breadcrumbs });
}
}
onFilter(object) {
if (!this.props.searchFilter) {
return true;
}
const searchFilter = this.props.searchFilter.toLowerCase();
// For bucket nodes, check if any of their children match
if (object instanceof BucketProperty) {
const { object: array, startIndex, endIndex } = object;
for (let i = startIndex; i <= endIndex; i++) {
const childJson = JSON.stringify(array[i]);
if (childJson.toLowerCase().includes(searchFilter)) {
return true;
}
}
return false;
}
const json = object.name + JSON.stringify(object.value);
return json.toLowerCase().includes(searchFilter);
}
renderValue(props) {
const member = props.member;
// Hide value for bucket nodes (they show ranges like [0…99])
if (member.type === "bucket") {
return null;
}
// Hide object summary when non-empty object is expanded (bug 1244912).
if (isObject(member.value) && member.hasChildren && member.open) {
return null;
}
// Render the value (summary) using Reps library.
return Rep(
Object.assign({}, props, {
cropLimit: MAX_STRING_LENGTH,
noGrip: true,
isInContentPage: true,
})
);
}
renderTree() {
// Append custom column for displaying values. This column
// Take all available horizontal space.
const columns = [
{
id: "value",
width: "100%",
},
];
// Render tree component.
return TreeView({
object: this.props.data,
mode: MODE.LONG,
bucketLargeArrays: true,
onFilter: this.onFilter,
columns,
renderValue: this.renderValue,
expandedNodes: this.props.expandedNodes,
maxStringLength: MAX_STRING_LENGTH,
onRowSelected: this.onRowSelected,
});
}
render() {
let content;
const data = this.props.data;
if (!isObject(data)) {
content = div(
{ className: "jsonPrimitiveValue" },
Rep({
object: data,
})
);
} else if (data instanceof Error) {
content = div({ className: "jsonParseError" }, data + "");
} else if (data.type === JSON_NUMBER) {
content = div(
{ className: "jsonPrimitiveValue" },
Rep({
object: data,
noGrip: true,
})
);
} else {
content = this.renderTree();
}
return div(
{ className: "jsonPanelBox tab-panel-inner" },
JsonToolbar({
actions: this.props.actions,
dataSize: this.props.dataSize,
}),
div(
{
className: "panelContent",
id: "json-scrolling-panel",
tabIndex: 0,
},
content
),
JsonBreadcrumb({
items: this.state.breadcrumbs,
})
);
}
}
// Helpers
/**
* Determines the type of a given value.
* Returns a string representing the type of the input value.
*
* @param {*} value - The value whose type is to be determined.
* @returns {string} The type of the input value as a string.
* `array`, `null`, `object`, `number`, `boolean`, `string`
*/
function getJsonValueType(value) {
if (value?.type === JSON_NUMBER) {
return "number";
}
if (Array.isArray(value)) {
return "array";
} else if (value === null) {
return "null";
}
return typeof value;
}
export default { JsonPanel };